2024-02-06 21:58:11 +01:00
package datadogsketches
import (
"fmt"
"math"
"strconv"
"github.com/VictoriaMetrics/easyproto"
)
var (
2024-02-07 20:33:18 +01:00
// These constants were obtained from https://github.com/DataDog/opentelemetry-mapping-go/blob/48d52eeea60d28da2e14c154a24557c4d290c6e2/pkg/quantile/config.go
eps = 1.0 / 128
gamma = 1 + 2 * eps
gammaLn = math . Log1p ( 2 * eps )
defaultMin = 1e-9
2024-02-06 21:58:11 +01:00
bias = 1 - int ( math . Floor ( math . Log ( defaultMin ) / gammaLn ) )
quantiles = [ ] float64 { 0.5 , 0.75 , 0.9 , 0.95 , 0.99 }
)
2024-02-07 00:15:25 +01:00
var quantilesStr = func ( ) [ ] string {
a := make ( [ ] string , len ( quantiles ) )
for i , q := range quantiles {
a [ i ] = strconv . FormatFloat ( q , 'g' , 3 , 64 )
}
return a
} ( )
// Label is a single label for Metric
type Label struct {
2024-02-06 21:58:11 +01:00
Name string
Value string
}
// Metric stores metrics extracted from sketches
type Metric struct {
Name string
2024-02-07 00:15:25 +01:00
Labels [ ] Label
Points [ ] Point
}
// Point stores a single point extracted from sketches
type Point struct {
Value float64
Timestamp int64
2024-02-06 21:58:11 +01:00
}
// SketchPayload stores sketches extracted from /api/beta/sketches endpoint
//
// message SketchPayload {
// repeated Sketch sketches = 1
// }
2024-02-07 00:15:25 +01:00
//
// See https://github.com/DataDog/agent-payload/blob/38db68d9641c8a0bd2e1eac53b9d54793448850f/proto/metrics/agent_payload.proto#L90
2024-02-06 21:58:11 +01:00
type SketchPayload struct {
Sketches [ ] * Sketch
}
2024-02-07 00:15:25 +01:00
// UnmarshalProtobuf decodes src to SketchPayload struct
2024-02-06 21:58:11 +01:00
func ( sp * SketchPayload ) UnmarshalProtobuf ( src [ ] byte ) ( err error ) {
2024-02-07 00:15:25 +01:00
sp . Sketches = nil
2024-02-06 21:58:11 +01:00
var fc easyproto . FieldContext
for len ( src ) > 0 {
src , err = fc . NextField ( src )
if err != nil {
return fmt . Errorf ( "cannot read next field in SketchPayload message: %w" , err )
}
switch fc . FieldNum {
case 1 :
data , ok := fc . MessageData ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read Sketch data" )
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
var s Sketch
2024-02-06 21:58:11 +01:00
if err := s . unmarshalProtobuf ( data ) ; err != nil {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot unmarshal Sketch: %w" , err )
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
sp . Sketches = append ( sp . Sketches , & s )
2024-02-06 21:58:11 +01:00
}
}
return nil
}
// Sketch proto struct
//
// message Sketch {
// string metric = 1;
// string host = 2;
// repeated string tags = 4;
// repeated Dogsketch dogsketches = 7
// }
2024-02-07 00:15:25 +01:00
//
// See https://github.com/DataDog/agent-payload/blob/38db68d9641c8a0bd2e1eac53b9d54793448850f/proto/metrics/agent_payload.proto#L91
2024-02-06 21:58:11 +01:00
type Sketch struct {
Metric string
Host string
Tags [ ] string
Dogsketches [ ] * Dogsketch
}
2024-02-07 00:15:25 +01:00
// unmarshalProtobuf decodes src to Sketch struct
2024-02-06 21:58:11 +01:00
func ( s * Sketch ) unmarshalProtobuf ( src [ ] byte ) ( err error ) {
s . Metric = ""
s . Host = ""
2024-02-07 00:15:25 +01:00
s . Tags = nil
s . Dogsketches = nil
2024-02-06 21:58:11 +01:00
var fc easyproto . FieldContext
for len ( src ) > 0 {
src , err = fc . NextField ( src )
if err != nil {
return fmt . Errorf ( "cannot read next field in Sketch message: %w" , err )
}
switch fc . FieldNum {
case 1 :
metric , ok := fc . String ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read metric" )
2024-02-06 21:58:11 +01:00
}
s . Metric = metric
case 2 :
host , ok := fc . String ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read host" )
2024-02-06 21:58:11 +01:00
}
s . Host = host
case 4 :
tag , ok := fc . String ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read tag" )
2024-02-06 21:58:11 +01:00
}
s . Tags = append ( s . Tags , tag )
case 7 :
data , ok := fc . MessageData ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read Dogsketch data" )
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
var d Dogsketch
2024-02-06 21:58:11 +01:00
if err := d . unmarshalProtobuf ( data ) ; err != nil {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot unmarshal Dogsketch: %w" , err )
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
s . Dogsketches = append ( s . Dogsketches , & d )
2024-02-06 21:58:11 +01:00
}
}
return nil
}
2024-02-07 00:15:25 +01:00
// RowsCount returns the number of samples s generates.
2024-02-06 21:58:11 +01:00
func ( s * Sketch ) RowsCount ( ) int {
2024-02-07 00:15:25 +01:00
// The sketch contains len(quantiles) plus *_sum and *_count metrics
// per each Dogsketch in s.Dogsketches.
return ( len ( quantiles ) + 2 ) * len ( s . Dogsketches )
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
// ToSummary generates Prometheus summary from the given s.
func ( s * Sketch ) ToSummary ( ) [ ] * Metric {
metrics := make ( [ ] * Metric , len ( quantiles ) + 2 )
dogsketches := s . Dogsketches
sumPoints := make ( [ ] Point , len ( dogsketches ) )
countPoints := make ( [ ] Point , len ( dogsketches ) )
metrics [ len ( metrics ) - 2 ] = & Metric {
Name : s . Metric + "_sum" ,
Points : sumPoints ,
}
metrics [ len ( metrics ) - 1 ] = & Metric {
Name : s . Metric + "_count" ,
Points : countPoints ,
2024-02-06 21:58:11 +01:00
}
2024-02-07 00:15:25 +01:00
for i , q := range quantiles {
points := make ( [ ] Point , len ( dogsketches ) )
for j , d := range dogsketches {
timestamp := d . Ts * 1000
points [ j ] = Point {
Timestamp : timestamp ,
2024-02-07 20:33:18 +01:00
Value : d . quantile ( q ) ,
2024-02-07 00:15:25 +01:00
}
sumPoints [ j ] = Point {
Timestamp : timestamp ,
Value : d . Sum ,
}
countPoints [ j ] = Point {
Timestamp : timestamp ,
Value : float64 ( d . Cnt ) ,
}
}
metrics [ i ] = & Metric {
2024-02-06 21:58:11 +01:00
Name : s . Metric ,
2024-02-07 00:15:25 +01:00
Labels : [ ] Label { {
2024-02-06 21:58:11 +01:00
Name : "quantile" ,
2024-02-07 00:15:25 +01:00
Value : quantilesStr [ i ] ,
2024-02-06 21:58:11 +01:00
} } ,
2024-02-07 00:15:25 +01:00
Points : points ,
2024-02-06 21:58:11 +01:00
}
}
2024-02-07 00:15:25 +01:00
return metrics
2024-02-06 21:58:11 +01:00
}
// Dogsketch proto struct
//
// message Dogsketch {
// int64 ts = 1;
// int64 cnt = 2;
// double min = 3;
// double max = 4;
// double sum = 6;
// repeated sint32 k = 7;
// repeated uint32 n = 8;
// }
2024-02-07 00:15:25 +01:00
//
// See https://github.com/DataDog/agent-payload/blob/38db68d9641c8a0bd2e1eac53b9d54793448850f/proto/metrics/agent_payload.proto#L104
2024-02-06 21:58:11 +01:00
type Dogsketch struct {
Ts int64
Cnt int64
Min float64
Max float64
Sum float64
K [ ] int32
N [ ] uint32
}
2024-02-07 00:15:25 +01:00
// unmarshalProtobuf decodes src to Dogsketch struct
2024-02-06 21:58:11 +01:00
func ( d * Dogsketch ) unmarshalProtobuf ( src [ ] byte ) ( err error ) {
d . Ts = 0
d . Cnt = 0
d . Min = 0.0
d . Max = 0.0
d . Sum = 0.0
2024-02-07 00:15:25 +01:00
d . K = nil
d . N = nil
2024-02-06 21:58:11 +01:00
var fc easyproto . FieldContext
for len ( src ) > 0 {
src , err = fc . NextField ( src )
if err != nil {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read next field in Dogsketch message: %w" , err )
2024-02-06 21:58:11 +01:00
}
switch fc . FieldNum {
case 1 :
ts , ok := fc . Int64 ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read timestamp" )
2024-02-06 21:58:11 +01:00
}
d . Ts = ts
case 2 :
cnt , ok := fc . Int64 ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read count" )
2024-02-06 21:58:11 +01:00
}
d . Cnt = cnt
case 3 :
min , ok := fc . Double ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read min" )
2024-02-06 21:58:11 +01:00
}
d . Min = min
case 4 :
max , ok := fc . Double ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read max" )
2024-02-06 21:58:11 +01:00
}
d . Max = max
case 6 :
sum , ok := fc . Double ( )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read sum" )
2024-02-06 21:58:11 +01:00
}
d . Sum = sum
case 7 :
var ok bool
d . K , ok = fc . UnpackSint32s ( d . K )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read k" )
2024-02-06 21:58:11 +01:00
}
case 8 :
var ok bool
d . N , ok = fc . UnpackUint32s ( d . N )
if ! ok {
2024-02-07 00:15:25 +01:00
return fmt . Errorf ( "cannot read n" )
2024-02-06 21:58:11 +01:00
}
}
}
return nil
}
2024-02-07 20:33:18 +01:00
// This function has been copied from https://github.com/DataDog/opentelemetry-mapping-go/blob/48d52eeea60d28da2e14c154a24557c4d290c6e2/pkg/quantile/sparse.go#L92
func ( d * Dogsketch ) quantile ( q float64 ) float64 {
2024-02-06 21:58:11 +01:00
switch {
case d . Cnt == 0 :
return 0
2024-02-07 00:15:25 +01:00
case q <= 0 :
2024-02-06 21:58:11 +01:00
return d . Min
2024-02-07 00:15:25 +01:00
case q >= 1 :
2024-02-06 21:58:11 +01:00
return d . Max
}
2024-02-07 00:15:25 +01:00
ns := d . N
ks := d . K
if len ( ns ) != len ( ks ) {
// Avoid index out of range panic in the loop below.
return 0
}
2024-02-07 20:43:24 +01:00
rank := math . RoundToEven ( q * float64 ( d . Cnt - 1 ) )
2024-02-07 00:15:25 +01:00
cnt := float64 ( 0 )
for i , n := range ns {
cnt += float64 ( n )
2024-02-06 21:58:11 +01:00
if cnt <= rank {
continue
}
2024-02-07 00:15:25 +01:00
weight := ( cnt - rank ) / float64 ( n )
vLow := f64 ( ks [ i ] )
2024-02-06 21:58:11 +01:00
vHigh := vLow * gamma
2024-02-07 20:33:18 +01:00
if i == 0 {
2024-02-06 21:58:11 +01:00
vLow = d . Min
}
return vLow * weight + vHigh * ( 1 - weight )
}
return d . Max
}
2024-02-07 20:33:18 +01:00
// This function has been copied from https://github.com/DataDog/opentelemetry-mapping-go/blob/48d52eeea60d28da2e14c154a24557c4d290c6e2/pkg/quantile/config.go#L54
2024-02-06 21:58:11 +01:00
func f64 ( k int32 ) float64 {
2024-02-07 20:33:18 +01:00
// See https://github.com/DataDog/opentelemetry-mapping-go/blob/48d52eeea60d28da2e14c154a24557c4d290c6e2/pkg/quantile/key.go#L14
if k <= - ( ( 1 << 15 ) - 1 ) {
return math . Inf ( - 1 )
}
if k >= ( ( 1 << 15 ) - 1 ) {
return math . Inf ( 1 )
}
if k == 0 {
2024-02-06 21:58:11 +01:00
return 0
}
2024-02-07 20:33:18 +01:00
if k < 0 {
return - f64 ( - k )
}
2024-02-06 21:58:11 +01:00
exp := float64 ( int ( k ) - bias )
return math . Pow ( gamma , exp )
}