From 41526ab363295f67fec15f5acde79609de3f7d78 Mon Sep 17 00:00:00 2001 From: Sheridan C Rawlins Date: Wed, 20 Nov 2024 09:21:39 -0800 Subject: [PATCH] Integrate with open telemetry - See #4062 --- config/config.go | 6 + go.mod | 22 +- go.sum | 19 +- metrics/config/metrics.go | 9 + metrics/opentelemetry/engine.go | 678 +++++++++++++++++++++++++++ metrics/opentelemetry/engine_test.go | 38 ++ metrics/opentelemetry/metrics.go | 158 +++++++ 7 files changed, 921 insertions(+), 9 deletions(-) create mode 100644 metrics/opentelemetry/engine.go create mode 100644 metrics/opentelemetry/engine_test.go create mode 100644 metrics/opentelemetry/metrics.go diff --git a/config/config.go b/config/config.go index 30f8d69adc1..7376f6324cf 100644 --- a/config/config.go +++ b/config/config.go @@ -544,9 +544,15 @@ type RequestTimeoutHeaders struct { RequestTimeoutInQueue string `mapstructure:"request_timeout_in_queue"` } +type OtelMetrics struct { + Enabled bool `mapstructure:"enabled"` + Prefix string `mapstructure:"prefix"` +} + type Metrics struct { Influxdb InfluxMetrics `mapstructure:"influxdb"` Prometheus PrometheusMetrics `mapstructure:"prometheus"` + Otel OtelMetrics `mapstructure:"otel"` Disabled DisabledMetrics `mapstructure:"disabled_metrics"` } diff --git a/go.mod b/go.mod index e844fe5bf25..dc2247dc624 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/prebid/prebid-server/v3 -go 1.21 +go 1.22 + +toolchain go1.23.3 retract v3.0.0 // Forgot to update major version in import path and module name require ( + github.com/51Degrees/device-detection-go/v4 v4.4.35 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/IABTechLab/adscert v0.34.0 github.com/NYTimes/gziphandler v1.1.1 @@ -16,6 +19,7 @@ require ( github.com/coocood/freecache v1.2.1 github.com/docker/go-units v0.4.0 github.com/go-sql-driver/mysql v1.6.0 + github.com/gobeam/stringy v0.0.7 github.com/gofrs/uuid v4.2.0+incompatible github.com/golang/glog v1.1.0 github.com/google/go-cmp v0.6.0 @@ -32,11 +36,16 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/rs/cors v1.11.0 + github.com/spf13/cast v1.5.0 github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/gjson v1.17.1 + github.com/tidwall/sjson v1.2.5 github.com/vrischmann/go-metrics-influxdb v0.1.1 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yudai/gojsondiff v1.0.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/metric v1.32.0 golang.org/x/net v0.23.0 golang.org/x/text v0.14.0 google.golang.org/grpc v1.56.3 @@ -45,11 +54,12 @@ require ( ) require ( - github.com/51Degrees/device-detection-go/v4 v4.4.35 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d // indirect @@ -65,19 +75,17 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.3.0 // indirect - github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/sys v0.18.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index a6a6226c616..1ab02e319bb 100644 --- a/go.sum +++ b/go.sum @@ -154,11 +154,18 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gobeam/stringy v0.0.7 h1:TD8SfhedUoiANhW88JlJqfrMsihskIRpU/VTsHGnAps= +github.com/gobeam/stringy v0.0.7/go.mod h1:W3620X9dJHf2FSZF5fRnWekHcHQjwmCz8ZQ2d1qloqE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -474,8 +481,9 @@ github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiu github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -484,8 +492,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= @@ -528,6 +537,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go index dcb330c47f9..3312f6dbd61 100644 --- a/metrics/config/metrics.go +++ b/metrics/config/metrics.go @@ -1,10 +1,12 @@ package config import ( + "log/slog" "time" "github.com/prebid/prebid-server/v3/config" "github.com/prebid/prebid-server/v3/metrics" + "github.com/prebid/prebid-server/v3/metrics/opentelemetry" prometheusmetrics "github.com/prebid/prebid-server/v3/metrics/prometheus" "github.com/prebid/prebid-server/v3/openrtb_ext" gometrics "github.com/rcrowley/go-metrics" @@ -43,6 +45,13 @@ func NewMetricsEngine(cfg *config.Configuration, adapterList []openrtb_ext.Bidde returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus, cfg.Metrics.Disabled, syncerKeys, moduleStageNames) engineList = append(engineList, returnEngine.PrometheusMetrics) } + if cfg.Metrics.Otel.Enabled { + otelEngine, err := opentelemetry.NewEngine(cfg.Metrics.Otel.Prefix, &cfg.Metrics.Disabled) + if err != nil { + slog.Error("error creating otel engine", "err", err.Error()) + } + engineList = append(engineList, otelEngine) + } // Now return the proper metrics engine if len(engineList) > 1 { diff --git a/metrics/opentelemetry/engine.go b/metrics/opentelemetry/engine.go new file mode 100644 index 00000000000..9cbc9fc2dc5 --- /dev/null +++ b/metrics/opentelemetry/engine.go @@ -0,0 +1,678 @@ +package opentelemetry + +import ( + "context" + "log/slog" + "strings" + "time" + + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/metrics" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +type ( + PbsMetrics struct { + ConnectionsClosed metric.Float64Counter `description:"Count of successful connections closed to Prebid Server" unit:"1"` + ConnectionsError metric.Float64Counter `description:"Count of errors for connection open and close attempts to Prebid Server labeled by type" unit:"1"` + ConnectionsOpened metric.Float64Counter `description:"Count of successful connections opened to Prebid Server" unit:"1"` + TmaxTimeout metric.Float64Counter `description:"Count of requests rejected due to Tmax timeout exceed" unit:"1"` + CookieSyncRequests metric.Float64Counter `description:"Count of cookie sync requests to Prebid Server" unit:"1"` + SetuidRequests metric.Float64Counter `description:"Count of set uid requests to Prebid Server" unit:"1"` + ImpressionsRequests metric.Float64Counter `description:"Count of requested impressions to Prebid Server labeled by type" unit:"1"` + + PrebidcacheWriteTime metric.Float64Histogram `description:"Seconds to write to Prebid Cache labeled by success or failure. Failure timing is limited by Prebid Server enforced timeouts" unit:"s" buckets:"0.001,0.002,0.005,0.01,0.025,0.05,0.1,0.2,0.3,0.4,0.5,1"` + + Requests metric.Float64Counter `description:"Count of total requests to Prebid Server labeled by type and status" unit:"1"` + DebugRequests metric.Float64Counter `description:"Count of total requests to Prebid Server that have debug enabled" unit:"1"` + RequestTime metric.Float64Histogram `description:"Seconds to resolve successful Prebid Server requests labeled by type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + RequestsWithoutCookie metric.Float64Counter `description:"Count of total requests to Prebid Server without a cookie labeled by type" unit:"1"` + + StoredImpressionsCachePerformance metric.Float64Counter `description:"Count of stored impression cache requests attempts by hits or miss" unit:"1"` + StoredRequestCachePerformance metric.Float64Counter `description:"Count of stored request cache requests attempts by hits or miss" unit:"1"` + AccountCachePerformance metric.Float64Counter `description:"Count of account cache lookups by hits or miss" unit:"1"` + StoredAccountFetchTime metric.Float64Histogram `description:"Seconds to fetch stored accounts labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredAccountErrors metric.Float64Counter `description:"Count of stored account errors by error type" unit:"1"` + StoredAmpFetchTime metric.Float64Histogram `description:"Seconds to fetch stored AMP requests labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredAmpErrors metric.Float64Counter `description:"Count of stored AMP errors by error type" unit:"1"` + StoredCategoryFetchTime metric.Float64Histogram `description:"Seconds to fetch stored categories labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredCategoryErrors metric.Float64Counter `description:"Count of stored category errors by error type" unit:"1"` + StoredRequestFetchTime metric.Float64Histogram `description:"Seconds to fetch stored requests labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredRequestErrors metric.Float64Counter `description:"Count of stored request errors by error type" unit:"1"` + StoredVideoFetchTime metric.Float64Histogram `description:"Seconds to fetch stored video labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredVideoErrors metric.Float64Counter `description:"Count of stored video errors by error type" unit:"1"` + + TimeoutNotification metric.Float64Counter `description:"Count of timeout notifications triggered, and if they were successfully sent" unit:"1"` + DnsLookupTime metric.Float64Histogram `description:"Seconds to resolve DNS" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + TlsHandhakeTime metric.Float64Histogram `description:"Seconds to perform TLS Handshake" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + + PrivacyCCPA metric.Float64Counter `description:"Count of total requests to Prebid Server where CCPA was provided by source and opt-out" unit:"1"` + PrivacyCOPPA metric.Float64Counter `description:"Count of total requests to Prebid Server where the COPPA flag was set by source" unit:"1"` + PrivacyTCF metric.Float64Counter `description:"Count of TCF versions for requests where GDPR was enforced by source and version" unit:"1"` + PrivacyLMT metric.Float64Counter `description:"Count of total requests to Prebid Server where the LMT flag was set by source" unit:"1"` + + AdapterBuyeruidsScrubbed metric.Float64Counter `description:"Count of total bidder requests with a scrubbed buyeruid due to a privacy policy" unit:"1"` + AdapterGdprRequestsBlocked metric.Float64Counter `description:"Count of total bidder requests blocked due to unsatisfied GDPR purpose 2 legal basis" unit:"1"` + + StoredResponsesFetchTime metric.Float64Histogram `description:"Seconds to fetch stored responses labeled by fetch type" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + StoredResponsesErrors metric.Float64Counter `description:"Count of stored response errors by error type" unit:"1"` + StoredResponses metric.Float64Counter `description:"Count of total requests to Prebid Server that have stored responses" unit:"1"` + + AdapterBids metric.Float64Counter `description:"Count of bids labeled by adapter and markup delivery type (adm or nurl)" unit:"1"` + AdapterErrors metric.Float64Counter `description:"Count of errors labeled by adapter and error type" unit:"1"` + AdapterPanics metric.Float64Counter `description:"Count of panics labeled by adapter" unit:"1"` + AdapterPrices metric.Float64Histogram `description:"Monetary value of the bids labeled by adapter" unit:"$" buckets:"250,500,750,1000,1500,2000,2500,3000,3500,4000"` + AdapterRequests metric.Float64Counter `description:"Count of requests labeled by adapter, if has a cookie, and if it resulted in bids" unit:"1"` + AdapterConnectionCreated metric.Float64Counter `description:"Count that keeps track of new connections when contacting adapter bidder endpoints" unit:"1"` + AdapterConnectionReused metric.Float64Counter `description:"Count that keeps track of reused connections when contacting adapter bidder endpoints" unit:"1"` + AdapterConnectionWait metric.Float64Histogram `description:"Seconds from when the connection was requested until it is either created or reused" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + AdapterResponseValidationSizeErr metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight" unit:"1"` + AdapterResponseValidationSizeWarn metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight (warn)" unit:"1"` + AdapterResponseValidationSecureErr metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a invalid bidAdm" unit:"1"` + AdapterResponseValidationSecureWarn metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a invalid bidAdm (warn)" unit:"1"` + + OverheadTime metric.Float64Histogram `description:"Seconds to prepare adapter request or resolve adapter response" unit:"s" buckets:"0.05,0.06,0.07,0.08,0.09,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1"` + AdapterRequestTime metric.Float64Histogram `description:"Seconds to resolve each successful request labeled by adapter" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + BidderServerResponseTime metric.Float64Histogram `description:"Duration needed to send HTTP request and receive response back from bidder server" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + SyncerRequests metric.Float64Counter `description:"Count of cookie sync requests where a syncer is a candidate to be synced labeled by syncer key and status" unit:"1"` + SyncerSets metric.Float64Counter `description:"Count of setuid set requests for a syncer labeled by syncer key and status" unit:"1"` + + AccountRequests metric.Float64Counter `description:"Count of total requests to Prebid Server labeled by account" unit:"1"` + AccountDebugRequests metric.Int64Counter `description:"Count of total requests to Prebid Server that have debug enabled labled by account" unit:"1"` + AccountResponseValidationSizeErr metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight labeled by account (enforce)" unit:"1"` + AccountResponseValidationSizeWarn metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a creative size greater than maxWidth/maxHeight labeled by account (warn)" unit:"1"` + AccountResponseValidationSecureErr metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a invalid bidAdm labeled by account (enforce)" unit:"1"` + AccountResponseValidationSecureWarn metric.Float64Counter `description:"Count that tracks number of bids removed from bid response that had a invalid bidAdm labeled by account (warn)" unit:"1"` + + RequestsQueueTime metric.Float64Histogram `description:"Seconds request was waiting in queue" unit:"s" buckets:"0,1,5,30,60,120,180,240,300"` + AccountStoredResponses metric.Float64Counter `description:"Count of total requests to Prebid Server that have stored responses labled by account" unit:"1"` + AdsCertSignTime metric.Float64Histogram `description:"Seconds to generate an AdsCert header" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + AdsCertRequests metric.Float64Counter `description:"Count of AdsCert request, and if they were successfully sent" unit:"1"` + + // Module metrics - Note these are renamed so can use Int64Counter + Module struct { + Duration metric.Float64Histogram `description:"Amount of seconds a module processed a hook labeled by stage name" unit:"s" buckets:"0.05,0.1,0.15,0.20,0.25,0.3,0.4,0.5,0.75,1"` + Called metric.Int64Counter `description:"Count of module calls labeled by stage name" unit:"1"` + Failed metric.Int64Counter `description:"Count of module fails labeled by stage name" unit:"1"` + SuccessNoops metric.Int64Counter `description:"Count of module successful noops labeled by stage name" unit:"1"` + SuccessUpdates metric.Int64Counter `description:"Count of module successful updates labeled by stage name" unit:"1"` + SuccessRejects metric.Int64Counter `description:"Count of module successful rejects labeled by stage name" unit:"1"` + ExecutionErrors metric.Int64Counter `description:"Count of module execution errors labeled by stage name" unit:"1"` + Timeouts metric.Int64Counter `description:"Count of module timeouts labeled by stage name" unit:"1"` + } + } + PbsMetricsEngine struct { + *PbsMetrics + MetricsDisabled *config.DisabledMetrics + } +) + +const ( + markupDeliveryAdm = "adm" + markupDeliveryNurl = "nurl" +) + +const ( + accountLabel attribute.Key = "account" + actionLabel attribute.Key = "action" + adapterErrorLabel attribute.Key = "adapter_error" + adapterLabel attribute.Key = "adapter" + bidTypeLabel attribute.Key = "bid_type" + cacheResultLabel attribute.Key = "cache_result" + connectionErrorLabel attribute.Key = "connection_error" + cookieLabel attribute.Key = "cookie" + hasBidsLabel attribute.Key = "has_bids" + isAudioLabel attribute.Key = "audio" + isBannerLabel attribute.Key = "banner" + isNativeLabel attribute.Key = "native" + isVideoLabel attribute.Key = "video" + markupDeliveryLabel attribute.Key = "delivery" + optOutLabel attribute.Key = "opt_out" + overheadTypeLabel attribute.Key = "overhead_type" + privacyBlockedLabel attribute.Key = "privacy_blocked" + requestStatusLabel attribute.Key = "request_status" + requestTypeLabel attribute.Key = "request_type" + stageLabel attribute.Key = "stage" + statusLabel attribute.Key = "status" + successLabel attribute.Key = "success" + syncerLabel attribute.Key = "syncer" + versionLabel attribute.Key = "version" + + storedDataFetchTypeLabel attribute.Key = "stored_data_fetch_type" + storedDataErrorLabel attribute.Key = "stored_data_error" + + requestSuccessLabel attribute.Key = "requestAcceptedLabel" + requestRejectLabel attribute.Key = "requestRejectedLabel" + + sourceLabel attribute.Key = "source" + sourceRequest attribute.Key = "request" + + moduleLabel attribute.Key = "module" +) + +const ( + requestSuccessful = "ok" + requestFailed = "failed" +) + +var ( + _ metrics.MetricsEngine = &PbsMetricsEngine{} + + Meter = otel.Meter("github.com/prebid/prebid-server/v3") +) + +// NewEngine creates a new OpenTelemetry engine with the given prefix +func NewEngine(prefix string, disabledMetrics *config.DisabledMetrics) (*PbsMetricsEngine, error) { + ret := &PbsMetricsEngine{ + PbsMetrics: &PbsMetrics{}, + MetricsDisabled: disabledMetrics, + } + if err := InitMetrics(Meter, ret.PbsMetrics, prefix); err != nil { + return nil, err + } + return ret, nil +} + +func (o *PbsMetricsEngine) RecordConnectionAccept(success bool) { + ctx := context.Background() + if success { + o.ConnectionsOpened.Add(ctx, 1) + } else { + o.ConnectionsError.Add(ctx, 1, metric.WithAttributes(connectionErrorLabel.String("accept"))) + } +} + +func (o *PbsMetricsEngine) RecordTMaxTimeout() { + ctx := context.Background() + o.TmaxTimeout.Add(ctx, 1) +} + +func (o *PbsMetricsEngine) RecordConnectionClose(success bool) { + ctx := context.Background() + if success { + o.ConnectionsClosed.Add(ctx, 1) + } else { + o.ConnectionsError.Add(ctx, 1, metric.WithAttributes(connectionErrorLabel.String("close"))) + } +} + +func (o *PbsMetricsEngine) RecordRequest(labels metrics.Labels) { + ctx := context.Background() + o.Requests.Add(ctx, 1, metric.WithAttributes( + requestTypeLabel.String(string(labels.RType)), + requestStatusLabel.String(string(labels.RequestStatus)), + )) + + if labels.CookieFlag == metrics.CookieFlagNo { + o.RequestsWithoutCookie.Add(ctx, 1, metric.WithAttributes( + requestTypeLabel.String(string(labels.RType)), + )) + } + + if labels.PubID != metrics.PublisherUnknown { + o.AccountRequests.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(labels.PubID), + )) + } +} + +func (o *PbsMetricsEngine) RecordImps(labels metrics.ImpLabels) { + ctx := context.Background() + o.ImpressionsRequests.Add(ctx, 1, metric.WithAttributes( + isBannerLabel.Bool(labels.BannerImps), + isVideoLabel.Bool(labels.VideoImps), + isAudioLabel.Bool(labels.AudioImps), + isNativeLabel.Bool(labels.NativeImps), + )) +} + +func (o *PbsMetricsEngine) RecordRequestTime(labels metrics.Labels, length time.Duration) { + ctx := context.Background() + o.RequestTime.Record(ctx, length.Seconds(), metric.WithAttributes( + requestTypeLabel.String(string(labels.RType)), + )) +} + +func (o *PbsMetricsEngine) RecordOverheadTime(overHead metrics.OverheadType, length time.Duration) { + ctx := context.Background() + o.OverheadTime.Record(ctx, length.Seconds(), metric.WithAttributes( + overheadTypeLabel.String(string(overHead)), + )) +} + +func (o *PbsMetricsEngine) RecordAdapterRequest(labels metrics.AdapterLabels) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(labels.Adapter)) + o.AdapterRequests.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + cookieLabel.String(string(labels.CookieFlag)), + hasBidsLabel.Bool(labels.AdapterBids == metrics.AdapterBidPresent), + )) + + for adapterError := range labels.AdapterErrors { + o.AdapterErrors.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + adapterErrorLabel.String(string(adapterError)), + )) + } +} + +func (o *PbsMetricsEngine) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(adapterName)) + if connWasReused { + o.AdapterConnectionReused.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) + } else { + o.AdapterConnectionCreated.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) + } + + o.AdapterConnectionWait.Record(ctx, connWaitTime.Seconds(), metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) +} + +func (o *PbsMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { + ctx := context.Background() + o.DnsLookupTime.Record(ctx, dnsLookupTime.Seconds()) +} + +func (o *PbsMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) { + ctx := context.Background() + o.TlsHandhakeTime.Record(ctx, tlsHandshakeTime.Seconds()) +} + +func (o *PbsMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) { + ctx := context.Background() + o.BidderServerResponseTime.Record(ctx, bidderServerResponseTime.Seconds()) +} + +func (o *PbsMetricsEngine) RecordAdapterPanic(labels metrics.AdapterLabels) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(labels.Adapter)) + o.AdapterPanics.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) +} + +func (o *PbsMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { + ctx := context.Background() + markupDelivery := markupDeliveryNurl + if hasAdm { + markupDelivery = markupDeliveryAdm + } + lowerCasedAdapter := strings.ToLower(string(labels.Adapter)) + o.AdapterBids.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + markupDeliveryLabel.String(markupDelivery), + )) +} + +func (o *PbsMetricsEngine) RecordAdapterPrice(labels metrics.AdapterLabels, cpm float64) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(labels.Adapter)) + o.AdapterPrices.Record(ctx, cpm, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) +} + +func (o *PbsMetricsEngine) RecordAdapterTime(labels metrics.AdapterLabels, length time.Duration) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(labels.Adapter)) + o.AdapterRequestTime.Record(ctx, length.Seconds(), metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + )) +} + +func (o *PbsMetricsEngine) RecordCookieSync(status metrics.CookieSyncStatus) { + ctx := context.Background() + o.CookieSyncRequests.Add(ctx, 1, metric.WithAttributes( + statusLabel.String(string(status)), + )) +} + +func (o *PbsMetricsEngine) RecordSyncerRequest(key string, status metrics.SyncerCookieSyncStatus) { + ctx := context.Background() + o.SyncerRequests.Add(ctx, 1, metric.WithAttributes( + syncerLabel.String(key), + statusLabel.String(string(status)), + )) +} + +func (o *PbsMetricsEngine) RecordSetUid(status metrics.SetUidStatus) { + ctx := context.Background() + o.SetuidRequests.Add(ctx, 1, metric.WithAttributes( + statusLabel.String(string(status)), + )) +} + +func (o *PbsMetricsEngine) RecordSyncerSet(key string, status metrics.SyncerSetUidStatus) { + ctx := context.Background() + o.SyncerSets.Add(ctx, 1, metric.WithAttributes( + syncerLabel.String(key), + statusLabel.String(string(status)), + )) +} + +func (o *PbsMetricsEngine) RecordStoredReqCacheResult(cacheResult metrics.CacheResult, inc int) { + ctx := context.Background() + incVal := float64(inc) + o.StoredRequestCachePerformance.Add(ctx, incVal, metric.WithAttributes( + cacheResultLabel.String(string(cacheResult)), + )) +} + +func (o *PbsMetricsEngine) RecordStoredImpCacheResult(cacheResult metrics.CacheResult, inc int) { + ctx := context.Background() + incVal := float64(inc) + o.StoredImpressionsCachePerformance.Add(ctx, incVal, metric.WithAttributes( + cacheResultLabel.String(string(cacheResult)), + )) +} + +func (o *PbsMetricsEngine) RecordAccountCacheResult(cacheResult metrics.CacheResult, inc int) { + ctx := context.Background() + incVal := float64(inc) + o.AccountCachePerformance.Add(ctx, incVal, metric.WithAttributes( + cacheResultLabel.String(string(cacheResult)), + )) +} + +func (o *PbsMetricsEngine) RecordStoredDataFetchTime(labels metrics.StoredDataLabels, length time.Duration) { + ctx := context.Background() + var histogramPtr *metric.Float64Histogram + switch labels.DataType { + case metrics.AccountDataType: + histogramPtr = &o.StoredAccountFetchTime + case metrics.AMPDataType: + histogramPtr = &o.StoredAmpFetchTime + case metrics.CategoryDataType: + histogramPtr = &o.StoredCategoryFetchTime + case metrics.RequestDataType: + histogramPtr = &o.StoredRequestFetchTime + case metrics.VideoDataType: + histogramPtr = &o.StoredVideoFetchTime + case metrics.ResponseDataType: + histogramPtr = &o.StoredResponsesFetchTime + default: + slog.DebugContext(ctx, "unknown data type", "dataType", labels.DataType) + return + } + // Record the chosen histogram + (*histogramPtr).Record(ctx, length.Seconds(), metric.WithAttributes( + storedDataFetchTypeLabel.String(string(labels.DataFetchType)), + )) +} + +func (o *PbsMetricsEngine) RecordStoredDataError(labels metrics.StoredDataLabels) { + ctx := context.Background() + var counterPtr *metric.Float64Counter + switch labels.DataType { + case metrics.AccountDataType: + counterPtr = &o.StoredAccountErrors + case metrics.AMPDataType: + counterPtr = &o.StoredAmpErrors + case metrics.CategoryDataType: + counterPtr = &o.StoredCategoryErrors + case metrics.RequestDataType: + counterPtr = &o.StoredRequestErrors + case metrics.VideoDataType: + counterPtr = &o.StoredVideoErrors + case metrics.ResponseDataType: + counterPtr = &o.StoredResponsesErrors + default: + slog.DebugContext(ctx, "unknown data type", "dataType", labels.DataType) + return + } + // Record the chosen histogram + (*counterPtr).Add(ctx, 1, metric.WithAttributes( + storedDataErrorLabel.String(string(labels.Error)), + )) +} + +func (o *PbsMetricsEngine) RecordPrebidCacheRequestTime(success bool, length time.Duration) { + ctx := context.Background() + o.PrebidcacheWriteTime.Record(ctx, length.Seconds(), metric.WithAttributes( + successLabel.Bool(success), + )) +} + +func (o *PbsMetricsEngine) RecordRequestQueueTime(success bool, requestType metrics.RequestType, length time.Duration) { + ctx := context.Background() + successLabelFormatted := requestRejectLabel + if success { + successLabelFormatted = requestSuccessLabel + } + + o.RequestsQueueTime.Record(ctx, length.Seconds(), metric.WithAttributes( + requestTypeLabel.String(string(requestType)), + requestStatusLabel.String(string(successLabelFormatted)), + )) +} + +func (o *PbsMetricsEngine) RecordTimeoutNotice(success bool) { + ctx := context.Background() + successFormatted := requestFailed + if success { + successFormatted = requestSuccessful + } + o.TimeoutNotification.Add(ctx, 1, metric.WithAttributes( + successLabel.String(successFormatted), + )) +} + +func (o *PbsMetricsEngine) RecordRequestPrivacy(privacy metrics.PrivacyLabels) { + ctx := context.Background() + if privacy.CCPAProvided { + o.PrivacyCCPA.Add(ctx, 1, metric.WithAttributes( + sourceLabel.String(string(sourceRequest)), + optOutLabel.Bool(privacy.CCPAEnforced), + )) + } + if privacy.COPPAEnforced { + o.PrivacyCOPPA.Add(ctx, 1, metric.WithAttributes( + sourceLabel.String(string(sourceRequest)), + )) + } + if privacy.GDPREnforced { + o.PrivacyTCF.Add(ctx, 1, metric.WithAttributes( + versionLabel.String(string(privacy.GDPRTCFVersion)), + sourceLabel.String(string(sourceRequest)), + )) + + } + if privacy.LMTEnforced { + o.PrivacyLMT.Add(ctx, 1, metric.WithAttributes( + sourceLabel.String(string(sourceRequest)), + )) + } +} + +func (o *PbsMetricsEngine) RecordAdapterBuyerUIDScrubbed(adapterName openrtb_ext.BidderName) { + ctx := context.Background() + o.AdapterBuyeruidsScrubbed.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(string(adapterName)), + )) +} + +func (o *PbsMetricsEngine) RecordAdapterGDPRRequestBlocked(adapterName openrtb_ext.BidderName) { + if o.MetricsDisabled.AdapterGDPRRequestBlocked { + return + } + ctx := context.Background() + o.AdapterGdprRequestsBlocked.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(string(adapterName)), + )) +} + +func (o *PbsMetricsEngine) RecordDebugRequest(debugEnabled bool, pubId string) { + ctx := context.Background() + if debugEnabled { + o.DebugRequests.Add(ctx, 1) + if !o.MetricsDisabled.AccountDebug && pubId != metrics.PublisherUnknown { + o.AccountDebugRequests.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(pubId), + )) + } + } +} + +func (o *PbsMetricsEngine) RecordStoredResponse(pubId string) { + ctx := context.Background() + o.StoredResponses.Add(ctx, 1) + if !o.MetricsDisabled.AccountStoredResponses && pubId != metrics.PublisherUnknown { + o.AccountStoredResponses.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(pubId), + )) + } +} + +func (o *PbsMetricsEngine) RecordAdsCertReq(success bool) { + ctx := context.Background() + successFormatted := requestFailed + if success { + successFormatted = requestSuccessful + } + o.AdsCertRequests.Add(ctx, 1, metric.WithAttributes( + successLabel.String(successFormatted), + )) +} + +func (o *PbsMetricsEngine) RecordAdsCertSignTime(adsCertSignTime time.Duration) { + ctx := context.Background() + o.AdsCertSignTime.Record(ctx, adsCertSignTime.Seconds()) +} + +func (o *PbsMetricsEngine) RecordBidValidationCreativeSizeError(adapter openrtb_ext.BidderName, account string) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(adapter)) + o.AdapterResponseValidationSizeErr.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + successLabel.String(string(successLabel)), + )) + + if !o.MetricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown { + o.AccountResponseValidationSizeErr.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(account), + successLabel.String(string(successLabel)), + )) + } +} + +func (o *PbsMetricsEngine) RecordBidValidationCreativeSizeWarn(adapter openrtb_ext.BidderName, account string) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(adapter)) + o.AdapterResponseValidationSizeWarn.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + successLabel.String(string(successLabel)), + )) + + if !o.MetricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown { + o.AccountResponseValidationSizeWarn.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(account), + successLabel.String(string(successLabel)), + )) + } +} + +func (o *PbsMetricsEngine) RecordBidValidationSecureMarkupError(adapter openrtb_ext.BidderName, account string) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(adapter)) + o.AdapterResponseValidationSecureErr.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + successLabel.String(string(successLabel)), + )) + + if !o.MetricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown { + o.AccountResponseValidationSecureErr.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(account), + successLabel.String(string(successLabel)), + )) + } +} + +func (o *PbsMetricsEngine) RecordBidValidationSecureMarkupWarn(adapter openrtb_ext.BidderName, account string) { + ctx := context.Background() + lowerCasedAdapter := strings.ToLower(string(adapter)) + o.AdapterResponseValidationSecureWarn.Add(ctx, 1, metric.WithAttributes( + adapterLabel.String(lowerCasedAdapter), + successLabel.String(string(successLabel)), + )) + + if !o.MetricsDisabled.AccountAdapterDetails && account != metrics.PublisherUnknown { + o.AccountResponseValidationSecureWarn.Add(ctx, 1, metric.WithAttributes( + accountLabel.String(account), + successLabel.String(string(successLabel)), + )) + } +} + +func (o *PbsMetricsEngine) RecordModuleCalled(labels metrics.ModuleLabels, duration time.Duration) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.Called.Add(ctx, 1, attributesOpt) + o.Module.Duration.Record(ctx, duration.Seconds(), attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleFailed(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.Failed.Add(ctx, 1, attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleSuccessNooped(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.SuccessNoops.Add(ctx, 1, attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleSuccessUpdated(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.SuccessUpdates.Add(ctx, 1, attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleSuccessRejected(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.SuccessRejects.Add(ctx, 1, attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleExecutionError(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.ExecutionErrors.Add(ctx, 1, attributesOpt) +} + +func (o *PbsMetricsEngine) RecordModuleTimeout(labels metrics.ModuleLabels) { + ctx := context.Background() + attributesOpt := metric.WithAttributes( + stageLabel.String(labels.Stage), + moduleLabel.String(labels.Module), + ) + o.Module.Timeouts.Add(ctx, 1, attributesOpt) +} diff --git a/metrics/opentelemetry/engine_test.go b/metrics/opentelemetry/engine_test.go new file mode 100644 index 00000000000..d3bc7248b67 --- /dev/null +++ b/metrics/opentelemetry/engine_test.go @@ -0,0 +1,38 @@ +package opentelemetry + +import ( + "testing" + "time" + + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/metrics" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInstantiateMetrics(t *testing.T) { + t.Run("no prefix", func(t *testing.T) { + emptyPrefixEngine, err := NewEngine("", &config.DisabledMetrics{}) + require.NoError(t, err) + assert.NotNil(t, emptyPrefixEngine) + assert.Equal(t, "account_cache_performance", GetMetricName(emptyPrefixEngine.AccountCachePerformance)) + }) + t.Run("foobar prefix", func(t *testing.T) { + emptyPrefixEngine, err := NewEngine("foobar", &config.DisabledMetrics{}) + require.NoError(t, err) + assert.NotNil(t, emptyPrefixEngine) + assert.Equal(t, "foobar.account_cache_performance", GetMetricName(emptyPrefixEngine.AccountCachePerformance)) + }) + t.Run("FooBar prefix", func(t *testing.T) { + emptyPrefixEngine, err := NewEngine("FooBar", &config.DisabledMetrics{}) + require.NoError(t, err) + assert.NotNil(t, emptyPrefixEngine) + assert.Equal(t, "foo_bar.account_cache_performance", GetMetricName(emptyPrefixEngine.AccountCachePerformance)) + }) +} + +func TestRecordAdapterTime(t *testing.T) { + emptyPrefixEngine, err := NewEngine("", &config.DisabledMetrics{}) + require.NoError(t, err) + emptyPrefixEngine.RecordAdapterTime(metrics.AdapterLabels{}, time.Second) +} diff --git a/metrics/opentelemetry/metrics.go b/metrics/opentelemetry/metrics.go new file mode 100644 index 00000000000..3f4b8b29fed --- /dev/null +++ b/metrics/opentelemetry/metrics.go @@ -0,0 +1,158 @@ +package opentelemetry + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strconv" + "strings" + + "github.com/gobeam/stringy" + "go.opentelemetry.io/otel/metric" +) + +type ( + // Elem represents a metric element. + Elem struct { + Name string + Value reflect.Value + Tag reflect.StructTag + } +) + +// FindAllMetrics finds all metrics in the given struct. +func FindAllMetrics(m any, prefix string) <-chan Elem { + ret := make(chan Elem, 16) + + go func() { + defer close(ret) + + v := reflect.Indirect(reflect.ValueOf(m)) + q := []Elem{{Value: v, Name: prefix}} + for len(q) > 0 { + e := q[0] + q = q[1:] + if e.Value.Kind() == reflect.Struct { + t := e.Value.Type() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Type.Kind() == reflect.Struct { + q = append(q, Elem{Name: e.Name + f.Name + ".", Value: e.Value.Field(i)}) + } else { + ret <- Elem{Name: e.Name + f.Name, Value: e.Value.Field(i), Tag: f.Tag} + } + } + } + } + }() + + return ret +} + +// CreateMetric creates a metric. +func CreateMetric[F func(string, ...O) (M, error), O, M any](name string, f F, opts ...metric.InstrumentOption) (M, error) { + fOpts := make([]O, len(opts)) + for i, o := range opts { + fOpts[i] = o.(O) + } + return f(name, fOpts...) +} + +// CreateHistogramMetric creates a histogram metric. +func CreateHistogramMetric[F func(string, ...O) (M, error), O, M any](name string, f F, histOpts []metric.HistogramOption, opts ...metric.InstrumentOption) (M, error) { + histOptsLen := len(histOpts) + fOpts := make([]O, histOptsLen+len(opts)) + for i, o := range histOpts { + fOpts[i] = o.(O) + } + for i, o := range opts { + fOpts[histOptsLen+i] = o.(O) + } + return f(name, fOpts...) +} + +// MetricName returns the metric name in snake case. +func MetricName(name string) string { + return stringy.New(name).SnakeCase(".", ".").ToLower() +} + +// HistogramOptions returns the options for a histogram metric. +func HistogramOptions(elem Elem) []metric.HistogramOption { + if bb, ok := elem.Tag.Lookup("buckets"); ok { + floatStrings := strings.Split(bb, ",") + floats := make([]float64, len(floatStrings)) + var err error + for i, s := range floatStrings { + if floats[i], err = strconv.ParseFloat(s, 64); err != nil { + slog.Error("failed to parse bucket boundary", "boundary", s, "err", err.Error()) + return nil + } + } + return []metric.HistogramOption{metric.WithExplicitBucketBoundaries(floats...)} + } + return nil +} + +// InitMetrics initializes all metrics in the given struct (except those tagged with metric="-"). +func InitMetrics(meter metric.Meter, m any, prefix string) error { + if prefix != "" && !strings.HasSuffix(prefix, ".") { + prefix += "." + } + var err error + for elem := range FindAllMetrics(m, prefix) { + err = errors.Join(err, InitMetricElem(meter, elem, prefix)) + } + if err != nil { + return err + } + return nil +} + +// InitMetricElem initializes a single metric element - intended for callers that want to initialize a single metric +// possibly with altered Name or Tags. +func InitMetricElem(meter metric.Meter, elem Elem, prefix string) error { + if elem.Tag.Get("metric") == "-" { + slog.Debug("skipping metric", "name", elem.Name) + return nil + } + var opts []metric.InstrumentOption + if description, ok := elem.Tag.Lookup("description"); ok { + opts = append(opts, metric.WithDescription(description)) + } else { + return fmt.Errorf("missing description tag from %s", elem.Name) + } + if u, ok := elem.Tag.Lookup("unit"); ok { + opts = append(opts, metric.WithUnit(u)) + } + var m any + var err error + metricName := MetricName(elem.Name) + switch elem.Value.Type().String() { + case "metric.Int64Counter": + m, err = CreateMetric(metricName, meter.Int64Counter, opts...) + case "metric.Int64UpDownCounter": + m, err = CreateMetric(metricName, meter.Int64UpDownCounter, opts...) + case "metric.Int64Histogram": + m, err = CreateHistogramMetric(metricName, meter.Int64Histogram, HistogramOptions(elem), opts...) + case "metric.Float64Counter": + m, err = CreateMetric(metricName, meter.Float64Counter, opts...) + case "metric.Float64UpDownCounter": + m, err = CreateMetric(metricName, meter.Float64UpDownCounter, opts...) + case "metric.Float64Histogram": + m, err = CreateHistogramMetric(metricName, meter.Float64Histogram, HistogramOptions(elem), opts...) + default: + slog.Warn("unknown metric type; skipping", "type", elem.Value.Type().String()) + return nil + } + if err != nil { + return err + } + elem.Value.Set(reflect.ValueOf(m)) + return nil +} + +// GetMetricName returns the name assigned to the metric. +func GetMetricName[M any](m M) string { + return reflect.Indirect(reflect.ValueOf(m)).FieldByName("name").String() +}