From dde97eb76a507c2e36df3229376554df80fd0292 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Tue, 28 Jan 2025 08:23:45 -0500 Subject: [PATCH 01/11] contrib/internal/httptrace: add telemetry for inferred spans (#3119) Co-authored-by: Rodrigo Arguello --- contrib/internal/httptrace/httptrace.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index e5be0ddc4b..22ac0306b4 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -11,9 +11,11 @@ import ( "context" "fmt" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "net/http" "strconv" "strings" + "sync" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" @@ -27,6 +29,8 @@ var ( cfg = newConfig() ) +var reportTelemetryConfigOnce sync.Once + type inferredSpanCreatedCtxKey struct{} type FinishSpanFunc = func(status int, errorFn func(int) bool, opts ...tracer.FinishOption) @@ -37,6 +41,15 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. // Append our span options before the given ones so that the caller can "overwrite" them. // TODO(): rework span start option handling (https://github.com/DataDog/dd-trace-go/issues/1352) + // we cannot track the configuration in newConfig because it's called during init() and the the telemetry client + // is not initialized yet + reportTelemetryConfigOnce.Do(func() { + telemetry.GlobalClient.ConfigChange([]telemetry.Configuration{ + {Name: "inferred_proxy_services_enabled", Value: cfg.inferredProxyServicesEnabled}, + }) + log.Debug("internal/httptrace: telemetry.ConfigChange called with cfg: %v:", cfg) + }) + var ipTags map[string]string if cfg.traceClientIP { ipTags, _ = httpsec.ClientIPTags(r.Header, true, r.RemoteAddr) From f0ed5842146d5a9d0b9dfef2867007894c3094f5 Mon Sep 17 00:00:00 2001 From: Keisuke Umegaki <41987730+keisku@users.noreply.github.com> Date: Wed, 29 Jan 2025 00:32:31 +0900 Subject: [PATCH 02/11] Support valkey-go tracing (#3081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: keisku Co-authored-by: Dario Castañé Co-authored-by: Rodrigo Arguello --- .github/workflows/unit-integration-tests.yml | 6 + contrib/go-redis/redis.v7/redis.go | 4 +- contrib/go-redis/redis.v8/redis.go | 4 +- contrib/go-redis/redis/redis.go | 4 +- contrib/redis/go-redis.v9/redis.go | 4 +- contrib/valkey-go/example_test.go | 35 +++ contrib/valkey-go/option.go | 42 +++ contrib/valkey-go/valkey.go | 284 +++++++++++++++++ contrib/valkey-go/valkey_test.go | 304 +++++++++++++++++++ ddtrace/ext/app_types.go | 3 + ddtrace/ext/db.go | 19 ++ ddtrace/ext/tags.go | 5 +- ddtrace/tracer/option.go | 1 + ddtrace/tracer/option_test.go | 2 +- docker-compose.yaml | 6 + go.mod | 3 +- go.sum | 10 +- internal/exectracetest/go.mod | 2 +- internal/exectracetest/go.sum | 4 +- internal/orchestrion/_integration/go.sum | 4 +- 20 files changed, 725 insertions(+), 21 deletions(-) create mode 100644 contrib/valkey-go/example_test.go create mode 100644 contrib/valkey-go/option.go create mode 100644 contrib/valkey-go/valkey.go create mode 100644 contrib/valkey-go/valkey_test.go diff --git a/.github/workflows/unit-integration-tests.yml b/.github/workflows/unit-integration-tests.yml index 7594e86163..a27aa3c138 100644 --- a/.github/workflows/unit-integration-tests.yml +++ b/.github/workflows/unit-integration-tests.yml @@ -144,6 +144,12 @@ jobs: image: redis:3.2 ports: - 6379:6379 + valkey: + image: valkey/valkey:8 + env: + VALKEY_EXTRA_FLAGS: "--port 6380 --requirepass password-for-default" + ports: + - 6380:6380 elasticsearch2: image: elasticsearch:2 env: diff --git a/contrib/go-redis/redis.v7/redis.go b/contrib/go-redis/redis.v7/redis.go index 5376ae5e9c..4c003bebd3 100644 --- a/contrib/go-redis/redis.v7/redis.go +++ b/contrib/go-redis/redis.v7/redis.go @@ -79,7 +79,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } else { @@ -91,7 +91,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } diff --git a/contrib/go-redis/redis.v8/redis.go b/contrib/go-redis/redis.v8/redis.go index a3ec7bc1c0..aa3ba4e6e7 100644 --- a/contrib/go-redis/redis.v8/redis.go +++ b/contrib/go-redis/redis.v8/redis.go @@ -78,7 +78,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } else { @@ -90,7 +90,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), tracer.Tag(ext.RedisDatabaseIndex, opt.DB), } } diff --git a/contrib/go-redis/redis/redis.go b/contrib/go-redis/redis/redis.go index 56feb6dcfd..9e40db15ba 100644 --- a/contrib/go-redis/redis/redis.go +++ b/contrib/go-redis/redis/redis.go @@ -130,7 +130,7 @@ func (c *Pipeliner) execWithContext(ctx context.Context) ([]redis.Cmder, error) tracer.ResourceName("redis"), tracer.Tag(ext.TargetHost, p.host), tracer.Tag(ext.TargetPort, p.port), - tracer.Tag("out.db", strconv.Itoa(p.db)), + tracer.Tag(ext.TargetDB, strconv.Itoa(p.db)), tracer.Tag(ext.Component, componentName), tracer.Tag(ext.SpanKind, ext.SpanKindClient), tracer.Tag(ext.DBSystem, ext.DBSystemRedis), @@ -202,7 +202,7 @@ func createWrapperFromClient(tc *Client) func(oldProcess func(cmd redis.Cmder) e tracer.ResourceName(parts[0]), tracer.Tag(ext.TargetHost, p.host), tracer.Tag(ext.TargetPort, p.port), - tracer.Tag("out.db", strconv.Itoa(p.db)), + tracer.Tag(ext.TargetDB, strconv.Itoa(p.db)), tracer.Tag("redis.raw_command", raw), tracer.Tag("redis.args_length", strconv.Itoa(length)), tracer.Tag(ext.Component, componentName), diff --git a/contrib/redis/go-redis.v9/redis.go b/contrib/redis/go-redis.v9/redis.go index 97a4fa6b10..848371533a 100644 --- a/contrib/redis/go-redis.v9/redis.go +++ b/contrib/redis/go-redis.v9/redis.go @@ -78,7 +78,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio opt := clientOptions.Options() if opt.Addr == "FailoverClient" { additionalTags = []ddtrace.StartSpanOption{ - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), } } else { host, port, err := net.SplitHostPort(opt.Addr) @@ -89,7 +89,7 @@ func additionalTagOptions(client redis.UniversalClient) []ddtrace.StartSpanOptio additionalTags = []ddtrace.StartSpanOption{ tracer.Tag(ext.TargetHost, host), tracer.Tag(ext.TargetPort, port), - tracer.Tag("out.db", strconv.Itoa(opt.DB)), + tracer.Tag(ext.TargetDB, strconv.Itoa(opt.DB)), } } } else if clientOptions, ok := client.(clusterOptions); ok { diff --git a/contrib/valkey-go/example_test.go b/contrib/valkey-go/example_test.go new file mode 100644 index 0000000000..236a35c91c --- /dev/null +++ b/contrib/valkey-go/example_test.go @@ -0,0 +1,35 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package valkey_test + +import ( + "context" + "log" + + "github.com/valkey-io/valkey-go" + valkeytrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/valkey-go" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// To start tracing Valkey, simply create a new client using the library and continue +// using as you normally would. +func Example() { + tracer.Start() + defer tracer.Stop() + + vk, err := valkeytrace.NewClient(valkey.ClientOption{ + InitAddress: []string{"localhost:6379"}, + }) + if err != nil { + log.Fatal(err) + return + } + + if err := vk.Do(context.Background(), vk.B().Set().Key("key").Value("value").Build()).Error(); err != nil { + log.Fatal(err) + return + } +} diff --git a/contrib/valkey-go/option.go b/contrib/valkey-go/option.go new file mode 100644 index 0000000000..60766ef45a --- /dev/null +++ b/contrib/valkey-go/option.go @@ -0,0 +1,42 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package valkey + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema" +) + +type config struct { + rawCommand bool + serviceName string +} + +// Option represents an option that can be used to create or wrap a client. +type Option func(*config) + +func defaultConfig() *config { + return &config{ + // Do not include the raw command by default since it could contain sensitive data. + rawCommand: internal.BoolEnv("DD_TRACE_VALKEY_RAW_COMMAND", false), + serviceName: namingschema.ServiceName(defaultServiceName), + } +} + +// WithRawCommand can be used to set a tag `valkey.raw_command` in the created spans (disabled by default). +// Warning: please note the datadog-agent currently does not support obfuscation for this tag, so use this at your own risk. +func WithRawCommand(rawCommand bool) Option { + return func(cfg *config) { + cfg.rawCommand = rawCommand + } +} + +// WithServiceName sets the given service name for the client. +func WithServiceName(name string) Option { + return func(cfg *config) { + cfg.serviceName = name + } +} diff --git a/contrib/valkey-go/valkey.go b/contrib/valkey-go/valkey.go new file mode 100644 index 0000000000..5362b397e7 --- /dev/null +++ b/contrib/valkey-go/valkey.go @@ -0,0 +1,284 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Package valkey provides tracing functions for tracing the valkey-io/valkey-go package (https://github.com/valkey-io/valkey-go). +package valkey + +import ( + "context" + "net" + "strconv" + "strings" + "time" + + "github.com/valkey-io/valkey-go" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" +) + +const ( + componentName = "valkey-io/valkey-go" + defaultServiceName = "valkey.client" +) + +func init() { + telemetry.LoadIntegration(componentName) + tracer.MarkIntegrationImported("github.com/valkey-io/valkey-go") +} + +var ( + _ valkey.Client = (*client)(nil) +) + +type client struct { + client valkey.Client + cfg *config + host string + port string + dbIndex string + user string +} + +func (c *client) B() valkey.Builder { + return c.client.B() +} + +func (c *client) Close() { + c.client.Close() +} + +// NewClient returns a new valkey.Client enhanced with tracing. +func NewClient(clientOption valkey.ClientOption, opts ...Option) (valkey.Client, error) { + valkeyClient, err := valkey.NewClient(clientOption) + if err != nil { + return nil, err + } + cfg := defaultConfig() + for _, fn := range opts { + fn(cfg) + } + tClient := &client{ + client: valkeyClient, + cfg: cfg, + dbIndex: strconv.FormatInt(int64(clientOption.SelectDB), 10), + user: clientOption.Username, + } + if len(clientOption.InitAddress) > 0 { + host, port, err := net.SplitHostPort(clientOption.InitAddress[0]) + if err == nil { + tClient.host = host + tClient.port = port + } + } + return tClient, nil +} + +func (c *client) Do(ctx context.Context, cmd valkey.Completed) valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp := c.client.Do(ctx, cmd) + setClientCacheTags(span, resp) + span.Finish(tracer.WithError(resp.Error())) + return resp +} + +func (c *client) DoMulti(ctx context.Context, multi ...valkey.Completed) []valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommandMulti(multi)) + resp := c.client.DoMulti(ctx, multi...) + c.finishSpan(span, firstError(resp)) + return resp +} + +func (c *client) Receive(ctx context.Context, subscribe valkey.Completed, fn func(msg valkey.PubSubMessage)) error { + span, ctx := c.startSpan(ctx, processCommand(&subscribe)) + err := c.client.Receive(ctx, subscribe, fn) + c.finishSpan(span, err) + return err +} + +func (c *client) DoCache(ctx context.Context, cmd valkey.Cacheable, ttl time.Duration) valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp := c.client.DoCache(ctx, cmd, ttl) + setClientCacheTags(span, resp) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) DoMultiCache(ctx context.Context, multi ...valkey.CacheableTTL) []valkey.ValkeyResult { + span, ctx := c.startSpan(ctx, processCommandMultiCache(multi)) + resp := c.client.DoMultiCache(ctx, multi...) + c.finishSpan(span, firstError(resp)) + return resp +} + +func (c *client) DoStream(ctx context.Context, cmd valkey.Completed) (resp valkey.ValkeyResultStream) { + span, ctx := c.startSpan(ctx, processCommand(&cmd)) + resp = c.client.DoStream(ctx, cmd) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) DoMultiStream(ctx context.Context, multi ...valkey.Completed) valkey.MultiValkeyResultStream { + span, ctx := c.startSpan(ctx, processCommandMulti(multi)) + resp := c.client.DoMultiStream(ctx, multi...) + c.finishSpan(span, resp.Error()) + return resp +} + +func (c *client) Dedicated(fn func(valkey.DedicatedClient) error) error { + return c.client.Dedicated(func(dc valkey.DedicatedClient) error { + return fn(&dedicatedClient{ + client: c, + dedicatedClient: dc, + }) + }) +} + +func (c *client) Dedicate() (client valkey.DedicatedClient, cancel func()) { + dedicated, cancel := c.client.Dedicate() + return &dedicatedClient{ + client: c, + dedicatedClient: dedicated, + }, cancel +} + +func (c *client) Nodes() map[string]valkey.Client { + nodes := c.client.Nodes() + for addr, valkeyClient := range nodes { + host, port, _ := net.SplitHostPort(addr) + nodes[addr] = &client{ + client: valkeyClient, + cfg: c.cfg, + host: host, + port: port, + dbIndex: c.dbIndex, + user: c.user, + } + } + return nodes +} + +var ( + _ valkey.DedicatedClient = (*dedicatedClient)(nil) +) + +type dedicatedClient struct { + *client + dedicatedClient valkey.DedicatedClient +} + +func (c *dedicatedClient) SetPubSubHooks(hooks valkey.PubSubHooks) <-chan error { + return c.dedicatedClient.SetPubSubHooks(hooks) +} + +type command struct { + statement string + raw string +} + +func (c *client) startSpan(ctx context.Context, cmd command) (tracer.Span, context.Context) { + opts := []tracer.StartSpanOption{ + tracer.ServiceName(c.cfg.serviceName), + tracer.ResourceName(cmd.statement), + tracer.SpanType(ext.SpanTypeValkey), + tracer.Tag(ext.TargetHost, c.host), + tracer.Tag(ext.TargetPort, c.port), + tracer.Tag(ext.Component, componentName), + tracer.Tag(ext.SpanKind, ext.SpanKindClient), + tracer.Tag(ext.DBSystem, ext.DBSystemValkey), + tracer.Tag(ext.TargetDB, c.dbIndex), + } + if c.cfg.rawCommand { + opts = append(opts, tracer.Tag(ext.ValkeyRawCommand, cmd.raw)) + } + if c.host != "" { + opts = append(opts, tracer.Tag(ext.TargetHost, c.host)) + } + if c.port != "" { + opts = append(opts, tracer.Tag(ext.TargetPort, c.port)) + } + if c.user != "" { + opts = append(opts, tracer.Tag(ext.DBUser, c.user)) + } + return tracer.StartSpanFromContext(ctx, "valkey.command", opts...) +} + +func (c *client) finishSpan(span tracer.Span, err error) { + var opts []tracer.FinishOption + if err != nil && !valkey.IsValkeyNil(err) { + opts = append(opts, tracer.WithError(err)) + } + span.Finish(opts...) +} + +type commander interface { + Commands() []string +} + +func processCommand(cmd commander) command { + cmds := cmd.Commands() + if len(cmds) == 0 { + return command{} + } + statement := cmds[0] + raw := strings.Join(cmds, " ") + return command{ + statement: statement, + raw: raw, + } +} + +func processCommandMulti(multi []valkey.Completed) command { + var cmds []command + for _, cmd := range multi { + cmds = append(cmds, processCommand(&cmd)) + } + return multiCommand(cmds) +} + +func processCommandMultiCache(multi []valkey.CacheableTTL) command { + var cmds []command + for _, cmd := range multi { + cmds = append(cmds, processCommand(&cmd.Cmd)) + } + return multiCommand(cmds) +} + +func multiCommand(cmds []command) command { + // limit to the 5 first + if len(cmds) > 5 { + cmds = cmds[:5] + } + statement := strings.Builder{} + raw := strings.Builder{} + for i, cmd := range cmds { + statement.WriteString(cmd.statement) + raw.WriteString(cmd.raw) + if i != len(cmds)-1 { + statement.WriteString(" ") + raw.WriteString(" ") + } + } + return command{ + statement: statement.String(), + raw: raw.String(), + } +} + +func firstError(s []valkey.ValkeyResult) error { + for _, result := range s { + if err := result.Error(); err != nil && !valkey.IsValkeyNil(err) { + return err + } + } + return nil +} + +func setClientCacheTags(s tracer.Span, result valkey.ValkeyResult) { + s.SetTag(ext.ValkeyClientCacheHit, result.IsCacheHit()) + s.SetTag(ext.ValkeyClientCacheTTL, result.CacheTTL()) + s.SetTag(ext.ValkeyClientCachePTTL, result.CachePTTL()) + s.SetTag(ext.ValkeyClientCachePXAT, result.CachePXAT()) +} diff --git a/contrib/valkey-go/valkey_test.go b/contrib/valkey-go/valkey_test.go new file mode 100644 index 0000000000..19799bf5d6 --- /dev/null +++ b/contrib/valkey-go/valkey_test.go @@ -0,0 +1,304 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. +package valkey + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valkey-io/valkey-go" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" +) + +const ( + // See docker-compose.yaml + valkeyPort = 6380 + valkeyUsername = "default" + valkeyPassword = "password-for-default" +) + +var ( + valkeyAddrs = []string{fmt.Sprintf("127.0.0.1:%d", valkeyPort)} +) + +func TestMain(m *testing.M) { + _, ok := os.LookupEnv("INTEGRATION") + if !ok { + fmt.Println("--- SKIP: to enable integration test, set the INTEGRATION environment variable") + os.Exit(0) + } + os.Exit(m.Run()) +} + +func TestNewClient(t *testing.T) { + prevName := globalconfig.ServiceName() + defer globalconfig.SetServiceName(prevName) + globalconfig.SetServiceName("global-service") + + tests := []struct { + name string + opts []Option + runTest func(*testing.T, context.Context, valkey.Client) + assertSpans func(*testing.T, []mocktracer.Span) + wantServiceName string + }{ + { + name: "Test SET command with raw command", + opts: []Option{ + WithRawCommand(true), + WithServiceName("test-service"), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + assert.NoError(t, client.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "test-service", + }, + { + name: "Test SET command without raw command", + opts: nil, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + require.NoError(t, client.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Nil(t, span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test SET GET multi command", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + resp := client.DoMulti(ctx, client.B().Set().Key("test_key").Value("test_value").Build(), client.B().Get().Key("test_key").Build()) + require.Len(t, resp, 2) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET GET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value GET test_key", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test HMGET command with cache", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + assert.NoError(t, client.DoCache(ctx, client.B().Hmget().Key("mk").Field("1", "2").Cache(), time.Minute).Error()) + resp, err := client.DoCache(ctx, client.B().Hmget().Key("mk").Field("1", "2").Cache(), time.Minute).ToArray() + require.Len(t, resp, 2) + require.NoError(t, err) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 2) + + span := spans[0] + assert.Equal(t, "HMGET", span.Tag(ext.ResourceName)) + assert.Equal(t, "HMGET mk 1 2", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Greater(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + + span = spans[1] + assert.Equal(t, "HMGET", span.Tag(ext.ResourceName)) + assert.Equal(t, "HMGET mk 1 2", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, true, span.Tag(ext.ValkeyClientCacheHit)) + assert.Greater(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Greater(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test GET stream command", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + resp := client.DoStream(ctx, client.B().Get().Key("test_key").Build()) + require.NoError(t, resp.Error()) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "GET", span.Tag(ext.ResourceName)) + assert.Equal(t, "GET test_key", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test multi command should be limited to 5", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Nanosecond) + client.DoMulti( + ctxWithTimeout, + client.B().Set().Key("k1").Value("v1").Build(), + client.B().Get().Key("k1").Build(), + client.B().Set().Key("k2").Value("v2").Build(), + client.B().Get().Key("k2").Build(), + client.B().Set().Key("k3").Value("v3").Build(), + client.B().Get().Key("k3").Build(), + ) + cancel() + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET GET SET GET SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET k1 v1 GET k1 SET k2 v2 GET k2 SET k3 v3", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Equal(t, context.DeadlineExceeded, span.Tag(ext.Error).(error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test SUBSCRIBE command with timeout", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Millisecond) + require.EqualError(t, + context.DeadlineExceeded, + client.Receive(ctxWithTimeout, client.B().Subscribe().Channel("test_channel").Build(), func(msg valkey.PubSubMessage) {}).Error(), + ) + cancel() + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SUBSCRIBE", span.Tag(ext.ResourceName)) + assert.Equal(t, "SUBSCRIBE test_channel", span.Tag(ext.ValkeyRawCommand)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheHit)) + assert.Nil(t, span.Tag(ext.ValkeyClientCacheTTL)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePXAT)) + assert.Nil(t, span.Tag(ext.ValkeyClientCachePTTL)) + assert.Equal(t, context.DeadlineExceeded, span.Tag(ext.Error).(error)) + }, + wantServiceName: "global-service", + }, + { + name: "Test Dedicated client", + opts: []Option{ + WithRawCommand(true), + }, + runTest: func(t *testing.T, ctx context.Context, client valkey.Client) { + err := client.Dedicated(func(d valkey.DedicatedClient) error { + return d.Do(ctx, client.B().Set().Key("test_key").Value("test_value").Build()).Error() + }) + require.NoError(t, err) + }, + assertSpans: func(t *testing.T, spans []mocktracer.Span) { + require.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "SET", span.Tag(ext.ResourceName)) + assert.Equal(t, "SET test_key test_value", span.Tag(ext.ValkeyRawCommand)) + assert.Equal(t, false, span.Tag(ext.ValkeyClientCacheHit)) + assert.Less(t, span.Tag(ext.ValkeyClientCacheTTL), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePXAT), int64(0)) + assert.Less(t, span.Tag(ext.ValkeyClientCachePTTL), int64(0)) + assert.Nil(t, span.Tag(ext.Error)) + }, + wantServiceName: "global-service", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + valkeyClientOption := valkey.ClientOption{ + InitAddress: valkeyAddrs, + Username: valkeyUsername, + Password: valkeyPassword, + } + client, err := NewClient(valkeyClientOption, tt.opts...) + require.NoError(t, err) + + root, ctx := tracer.StartSpanFromContext(context.Background(), "test.root", tracer.ServiceName("test-service")) + tt.runTest(t, ctx, client) + root.Finish() // test.root exists in the last span. + + spans := mt.FinishedSpans() + tt.assertSpans(t, spans[:len(spans)-1]) + + for _, span := range spans { + if span.OperationName() == "test.root" { + continue + } + + // The following assertions are common to all spans + assert.Equal(t, tt.wantServiceName, span.Tag(ext.ServiceName)) + assert.Equal(t, "127.0.0.1", span.Tag(ext.TargetHost)) + assert.Equal(t, "6380", span.Tag(ext.TargetPort)) + assert.Equal(t, "0", span.Tag(ext.TargetDB)) + assert.Equal(t, "default", span.Tag(ext.DBUser)) + assert.Equal(t, "valkey.command", span.OperationName()) + assert.Equal(t, "client", span.Tag(ext.SpanKind)) + assert.Equal(t, "valkey", span.Tag(ext.SpanType)) + assert.Equal(t, "valkey-io/valkey-go", span.Tag(ext.Component)) + assert.Equal(t, "valkey", span.Tag(ext.DBSystem)) + } + }) + } + +} diff --git a/ddtrace/ext/app_types.go b/ddtrace/ext/app_types.go index eb6ded8f60..1313c78a76 100644 --- a/ddtrace/ext/app_types.go +++ b/ddtrace/ext/app_types.go @@ -51,6 +51,9 @@ const ( // also have a "redis.raw_command" tag. SpanTypeRedis = "redis" + // SpanTypeRedis marks a span as a Valkey operation. + SpanTypeValkey = "valkey" + // SpanTypeMemcached marks a span as a memcached operation. SpanTypeMemcached = "memcached" diff --git a/ddtrace/ext/db.go b/ddtrace/ext/db.go index c9a046f86d..ff9b2efe33 100644 --- a/ddtrace/ext/db.go +++ b/ddtrace/ext/db.go @@ -32,6 +32,7 @@ const ( DBSystemOtherSQL = "other_sql" DBSystemElasticsearch = "elasticsearch" DBSystemRedis = "redis" + DBSystemValkey = "valkey" DBSystemMongoDB = "mongodb" DBSystemCassandra = "cassandra" DBSystemConsulKV = "consul" @@ -57,6 +58,24 @@ const ( RedisDatabaseIndex = "db.redis.database_index" ) +// Valkey tags. +const ( + // ValkeyRawCommand allows to set the raw command for tags. + ValkeyRawCommand = "valkey.raw_command" + + // ValkeyClientCacheHit is the remaining TTL in seconds of client side cache. + ValkeyClientCacheHit = "db.valkey.client.cache.hit" + + // ValkeyClientCacheTTL captures the Time-To-Live (TTL) of a cached entry in the client. + ValkeyClientCacheTTL = "db.valkey.client.cache.ttl" + + // ValkeyClientCachePTTL is the remaining PTTL in seconds of client side cache. + ValkeyClientCachePTTL = "db.valkey.client.cache.pttl" + + // ValkeyClientCachePXAT is the remaining PXAT in seconds of client side cache. + ValkeyClientCachePXAT = "db.valkey.client.cache.pxat" +) + // Cassandra tags. const ( // CassandraQuery is the tag name used for cassandra queries. diff --git a/ddtrace/ext/tags.go b/ddtrace/ext/tags.go index 375d7df7b5..b070b2fcd6 100644 --- a/ddtrace/ext/tags.go +++ b/ddtrace/ext/tags.go @@ -9,7 +9,6 @@ package ext const ( // TargetHost sets the target host address. - // Deprecated: Use NetworkDestinationName instead for hostname and NetworkDestinationIP for IP addresses TargetHost = "out.host" // NetworkDestinationName is the remote hostname or similar where the outbound connection is being made to. @@ -19,9 +18,11 @@ const ( NetworkDestinationIP = "network.destination.ip" // TargetPort sets the target host port. - // Deprecated: Use NetworkDestinationPort instead. TargetPort = "out.port" + // TargetDB sets the target db. + TargetDB = "out.db" + // NetworkDestinationPort is the remote port number of the outbound connection. NetworkDestinationPort = "network.destination.port" diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 1efdcd2359..4982e5cdaf 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -99,6 +99,7 @@ var contribIntegrations = map[string]struct { "github.com/zenazn/goji": {"Goji", false}, "log/slog": {"log/slog", false}, "github.com/uptrace/bun": {"Bun", false}, + "github.com/valkey-io/valkey-go": {"Valkey", false}, } var ( diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index f8a1c41461..8662bed43b 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -271,7 +271,7 @@ func TestAgentIntegration(t *testing.T) { defer clearIntegrationsForTests() cfg.loadContribIntegrations(nil) - assert.Equal(t, 56, len(cfg.integrations)) + assert.Equal(t, 57, len(cfg.integrations)) for integrationName, v := range cfg.integrations { assert.False(t, v.Instrumented, "integrationName=%s", integrationName) } diff --git a/docker-compose.yaml b/docker-compose.yaml index 76680be932..8eae54d8a3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -41,6 +41,12 @@ services: image: redis:3.2 ports: - "6379:6379" + valkey: + image: valkey/valkey:8 + environment: + VALKEY_EXTRA_FLAGS: "--port 6380 --requirepass password-for-default" + ports: + - "6380:6380" elasticsearch2: image: elasticsearch:2 environment: diff --git a/go.mod b/go.mod index 764dcc4acb..7f1eab4f62 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/uptrace/bun v1.1.17 github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 github.com/urfave/negroni v1.0.0 + github.com/valkey-io/valkey-go v1.0.52 github.com/valyala/fasthttp v1.51.0 github.com/vektah/gqlparser/v2 v2.5.16 github.com/zenazn/goji v1.0.1 @@ -294,7 +295,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 2f6d7e6cb5..9ea20e2ff9 100644 --- a/go.sum +++ b/go.sum @@ -798,8 +798,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= -github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -988,6 +988,8 @@ github.com/uptrace/bun/dialect/sqlitedialect v1.1.17 h1:i8NFU9r8YuavNFaYlNqi4ppn github.com/uptrace/bun/dialect/sqlitedialect v1.1.17/go.mod h1:YF0FO4VVnY9GHNH6rM4r3STlVEBxkOc6L88Bm5X5mzA= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valkey-io/valkey-go v1.0.52 h1:ojrR736satGucqpllYzal8fUrNNROc11V10zokAyIYg= +github.com/valkey-io/valkey-go v1.0.52/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -1118,8 +1120,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/internal/exectracetest/go.mod b/internal/exectracetest/go.mod index 25b8aaf070..13527330d5 100644 --- a/internal/exectracetest/go.mod +++ b/internal/exectracetest/go.mod @@ -5,7 +5,7 @@ go 1.22.10 require ( github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b github.com/mattn/go-sqlite3 v1.14.18 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 gopkg.in/DataDog/dd-trace-go.v1 v1.70.3 ) diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index db01821d72..3d73ca60cf 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -231,8 +231,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/internal/orchestrion/_integration/go.sum b/internal/orchestrion/_integration/go.sum index 180a65562c..11a07a06c8 100644 --- a/internal/orchestrion/_integration/go.sum +++ b/internal/orchestrion/_integration/go.sum @@ -1785,8 +1785,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= From 46362edf16188fb18b44d7c6a3a9d7aa68b49a79 Mon Sep 17 00:00:00 2001 From: Hannah Kim Date: Tue, 28 Jan 2025 11:39:16 -0500 Subject: [PATCH 03/11] ci: add codeowners to gotestsum report (#3118) --- .github/actions/add-codeowners/codeowners.sh | 25 ++++++++++++++++++++ .github/actions/dd-ci-upload/action.yml | 4 ++++ 2 files changed, 29 insertions(+) create mode 100755 .github/actions/add-codeowners/codeowners.sh diff --git a/.github/actions/add-codeowners/codeowners.sh b/.github/actions/add-codeowners/codeowners.sh new file mode 100755 index 0000000000..f9cec066d4 --- /dev/null +++ b/.github/actions/add-codeowners/codeowners.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +for file in "$@"; do + temp_file="tempfile.xml" + + # force write a new line at the end of the gotestsum-report.xml, or else + # the loop will skip the last line. + # fixes issue with a missing + echo -e "\n" >> $file + + while read p; do + # we might try to report gotestsum-report.xml multiple times, so don't + # calculate codeowners more times than we need + if [[ "$p" =~ \]*\)>||") + echo "$new_line" >> "$temp_file" + else + echo "$p" >> "$temp_file" + fi + done < $file + + mv "$temp_file" $file +done \ No newline at end of file diff --git a/.github/actions/dd-ci-upload/action.yml b/.github/actions/dd-ci-upload/action.yml index 3318728c50..c9da152c47 100644 --- a/.github/actions/dd-ci-upload/action.yml +++ b/.github/actions/dd-ci-upload/action.yml @@ -42,6 +42,10 @@ runs: curl -L --fail "https://github.com/DataDog/datadog-ci/releases/latest/download/datadog-ci_${{ env.DD_CI_CLI_BUILD }}" --output datadog-ci chmod +x datadog-ci + - name: Add CodeOwners to JUnit files + shell: bash + run: cd ./.github/actions/add-codeowners && ./codeowners.sh ${{ inputs.files }} + - name: Upload the JUnit files shell: bash run: | From ddfe2d4f36998c2457341cb1b23bd61db78b5795 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Wed, 29 Jan 2025 15:28:36 +0100 Subject: [PATCH 04/11] fix(net/http: orchestrion): obfuscate query string parameters when auto-instrumenting (#3127) --- contrib/net/http/orchestrion.client.yml | 11 ++++++++++- internal/orchestrion/_integration/aws.v2/base.go | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/contrib/net/http/orchestrion.client.yml b/contrib/net/http/orchestrion.client.yml index 1d2b2d2322..9842a92761 100644 --- a/contrib/net/http/orchestrion.client.yml +++ b/contrib/net/http/orchestrion.client.yml @@ -55,6 +55,7 @@ aspects: ddtrace: gopkg.in/DataDog/dd-trace-go.v1/ddtrace os: os links: + - gopkg.in/DataDog/dd-trace-go.v1/internal - gopkg.in/DataDog/dd-trace-go.v1/internal/appsec - gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec - gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer @@ -87,6 +88,12 @@ aspects: //go:linkname __dd_httptrace_GetErrorCodesFromInput gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace.GetErrorCodesFromInput func __dd_httptrace_GetErrorCodesFromInput(string) func(int) bool + //go:linkname __dd_httptrace_UrlFromRequest gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace.UrlFromRequest + func __dd_httptrace_UrlFromRequest(*Request, bool) string + + //go:linkname __dd_internal_BoolEnv gopkg.in/DataDog/dd-trace-go.v1/internal.BoolEnv + func __dd_internal_BoolEnv(string, bool) bool + type __dd_tracer_HTTPHeadersCarrier Header func (c __dd_tracer_HTTPHeadersCarrier) Set(key, val string) { Header(c).Set(key, val) @@ -97,6 +104,8 @@ aspects: return statusCode >= 400 && statusCode < 500 } + var __dd_queryStringEnabled = __dd_internal_BoolEnv("DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING", true) + func init() { v := os.Getenv("DD_TRACE_HTTP_CLIENT_ERROR_STATUSES") if fn := __dd_httptrace_GetErrorCodesFromInput(v); fn != nil { @@ -130,7 +139,7 @@ aspects: __dd_tracer_SpanType(ext.SpanTypeHTTP), __dd_tracer_ResourceName(resourceName), __dd_tracer_Tag(ext.HTTPMethod, {{ $req }}.Method), - __dd_tracer_Tag(ext.HTTPURL, url.String()), + __dd_tracer_Tag(ext.HTTPURL, __dd_httptrace_UrlFromRequest({{ $req }}, __dd_queryStringEnabled)), __dd_tracer_Tag(ext.Component, "net/http"), __dd_tracer_Tag(ext.SpanKind, ext.SpanKindClient), __dd_tracer_Tag(ext.NetworkDestinationName, url.Hostname()), diff --git a/internal/orchestrion/_integration/aws.v2/base.go b/internal/orchestrion/_integration/aws.v2/base.go index 8f7af1872b..8d61a26240 100644 --- a/internal/orchestrion/_integration/aws.v2/base.go +++ b/internal/orchestrion/_integration/aws.v2/base.go @@ -9,7 +9,6 @@ package awsv2 import ( "context" - "fmt" "testing" "github.com/DataDog/dd-trace-go/internal/orchestrion/_integration/internal/containers" @@ -67,7 +66,7 @@ func (b *base) expectedTraces() trace.Traces { Meta: map[string]string{ "http.method": "POST", "http.status_code": "200", - "http.url": fmt.Sprintf("http://localhost:%s/", b.port), + "http.url": "/", "network.destination.name": "localhost", "component": "net/http", "span.kind": "client", From ec054a5cbf955e0f018ff9d2fb41a63c645558fb Mon Sep 17 00:00:00 2001 From: Andrew Glaude Date: Fri, 31 Jan 2025 08:38:32 -0500 Subject: [PATCH 05/11] tracer/stats: Only get CI tags when CI Visibility is enabled (#3129) --- ddtrace/tracer/stats.go | 7 ++++++- ddtrace/tracer/stats_test.go | 2 +- go.mod | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ddtrace/tracer/stats.go b/ddtrace/tracer/stats.go index bf8530a8d6..687059dcc3 100644 --- a/ddtrace/tracer/stats.go +++ b/ddtrace/tracer/stats.go @@ -70,12 +70,17 @@ func newConcentrator(c *config, bucketSize int64, statsdClient internal.StatsdCl env = "unknown-env" log.Debug("No DD Env found, normally the agent should have one") } + gitCommitSha := "" + if c.ciVisibilityEnabled { + // We only have this data if we're in CI Visibility + gitCommitSha = utils.GetCITags()[constants.GitCommitSHA] + } aggKey := stats.PayloadAggregationKey{ Hostname: c.hostname, Env: env, Version: c.version, ContainerID: "", // This intentionally left empty as the Agent will attach the container ID only in certain situations. - GitCommitSha: utils.GetCITags()[constants.GitCommitSHA], + GitCommitSha: gitCommitSha, ImageTag: "", } spanConcentrator := stats.NewSpanConcentrator(sCfg, time.Now()) diff --git a/ddtrace/tracer/stats_test.go b/ddtrace/tracer/stats_test.go index 42f0bc17b0..503552adcf 100644 --- a/ddtrace/tracer/stats_test.go +++ b/ddtrace/tracer/stats_test.go @@ -105,7 +105,7 @@ func TestConcentrator(t *testing.T) { t.Run("ciGitSha", func(t *testing.T) { utils.AddCITags(constants.GitCommitSHA, "DEADBEEF") transport := newDummyTransport() - c := newConcentrator(&config{transport: transport, env: "someEnv"}, (10 * time.Second).Nanoseconds(), &statsd.NoOpClientDirect{}) + c := newConcentrator(&config{transport: transport, env: "someEnv", ciVisibilityEnabled: true}, (10 * time.Second).Nanoseconds(), &statsd.NoOpClientDirect{}) assert.Len(t, transport.Stats(), 0) ss1, ok := c.newTracerStatSpan(&s1, nil) assert.True(t, ok) diff --git a/go.mod b/go.mod index 7f1eab4f62..21ac06b4e0 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module gopkg.in/DataDog/dd-trace-go.v1 go 1.22.0 +// This replace is a temporary workaround to deal with a breaking change here that is used by the datadog-agent +// It can safely be removed once this PR is released: https://github.com/DataDog/datadog-agent/pull/33370 +replace github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes => github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 + require ( cloud.google.com/go/pubsub v1.37.0 github.com/99designs/gqlgen v0.17.36 From 7dec46c4c42b0214afbd392d2b4b1b36deb42226 Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Fri, 31 Jan 2025 17:32:06 +0100 Subject: [PATCH 06/11] (fix) internal/civisibility: change http default values and reduce test time. (#3134) --- internal/civisibility/utils/net/client.go | 4 ++-- internal/civisibility/utils/net/http.go | 2 +- internal/civisibility/utils/net/http_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/civisibility/utils/net/client.go b/internal/civisibility/utils/net/client.go index 8bca5e2440..23113a893d 100644 --- a/internal/civisibility/utils/net/client.go +++ b/internal/civisibility/utils/net/client.go @@ -29,9 +29,9 @@ import ( const ( // DefaultMaxRetries is the default number of retries for a request. - DefaultMaxRetries int = 5 + DefaultMaxRetries int = 3 // DefaultBackoff is the default backoff time for a request. - DefaultBackoff time.Duration = 150 * time.Millisecond + DefaultBackoff time.Duration = 100 * time.Millisecond ) type ( diff --git a/internal/civisibility/utils/net/http.go b/internal/civisibility/utils/net/http.go index 71c402cd16..1d122a323a 100644 --- a/internal/civisibility/utils/net/http.go +++ b/internal/civisibility/utils/net/http.go @@ -358,7 +358,7 @@ func decompressData(data []byte) ([]byte, error) { // exponentialBackoff performs an exponential backoff with retries. func exponentialBackoff(retryCount int, initialDelay time.Duration) { - maxDelay := 30 * time.Second + maxDelay := 10 * time.Second delay := initialDelay * (1 << uint(retryCount)) // Exponential backoff if delay > maxDelay { delay = maxDelay diff --git a/internal/civisibility/utils/net/http_test.go b/internal/civisibility/utils/net/http_test.go index abf9ac4808..a9b8b155ed 100644 --- a/internal/civisibility/utils/net/http_test.go +++ b/internal/civisibility/utils/net/http_test.go @@ -786,9 +786,9 @@ func TestSendRequestWithInvalidRetryAfterHeader(t *testing.T) { func TestExponentialBackoffWithMaxDelay(t *testing.T) { start := time.Now() - exponentialBackoff(10, 1*time.Second) // Should be limited to maxDelay (30s) + exponentialBackoff(10, 1*time.Second) // Should be limited to maxDelay (10s) duration := time.Since(start) - assert.LessOrEqual(t, duration, 31*time.Second) + assert.LessOrEqual(t, duration, 11*time.Second) } func TestSendRequestWithContextTimeout(t *testing.T) { From 2ee7731c7ad9833b771f2b338d640c6c472215de Mon Sep 17 00:00:00 2001 From: Mikayla Toffler <46911781+mtoffl01@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:50:19 -0500 Subject: [PATCH 07/11] ddtrace/tracer: Fix panic on Extract when DD_TRACE_PROPAGATION_EXTRACT_FIRST=true and no trace headers present (#3128) --- ddtrace/tracer/textmap.go | 15 ++++++++---- ddtrace/tracer/textmap_test.go | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/ddtrace/tracer/textmap.go b/ddtrace/tracer/textmap.go index f7367adb83..b1698ef48d 100644 --- a/ddtrace/tracer/textmap.go +++ b/ddtrace/tracer/textmap.go @@ -273,15 +273,22 @@ func (p *chainedPropagator) Inject(spanCtx ddtrace.SpanContext, carrier interfac func (p *chainedPropagator) Extract(carrier interface{}) (ddtrace.SpanContext, error) { var ctx ddtrace.SpanContext var links []ddtrace.SpanLink + for _, v := range p.extractors { firstExtract := (ctx == nil) // ctx stores the most recently extracted ctx across iterations; if it's nil, no extractor has run yet extractedCtx, err := v.Extract(carrier) + if firstExtract { - if err != nil && err != ErrSpanContextNotFound { // We only care if the first extraction returns an error because this breaks distributed tracing - return nil, err + if err != nil { + if p.onlyExtractFirst { // Every error is relevant when we are relying on the first extractor + return nil, err + } + if err != ErrSpanContextNotFound { // We don't care about ErrSpanContextNotFound because we could find a span context in a subsequent extractor + return nil, err + } } - if p.onlyExtractFirst { // Return early if only performing one extraction - return extractedCtx.(*spanContext), nil + if p.onlyExtractFirst { + return extractedCtx, nil } ctx = extractedCtx } else { // A local trace context has already been extracted diff --git a/ddtrace/tracer/textmap_test.go b/ddtrace/tracer/textmap_test.go index 74038fc0c7..0dd05124c0 100644 --- a/ddtrace/tracer/textmap_test.go +++ b/ddtrace/tracer/textmap_test.go @@ -2244,6 +2244,49 @@ func TestOtelPropagator(t *testing.T) { } } +// Assert that extraction returns a ErrSpanContextNotFound error when no trace context headers are found +func TestExtractNoHeaders(t *testing.T) { + tests := []struct { + name string + extractEnv string + extractFirst bool + }{ + { + name: "single header", + extractEnv: "datadog", + extractFirst: false, + }, + { + name: "single header - extractFirst", + extractEnv: "datadog", + extractFirst: true, + }, + { + name: "multi header", + extractEnv: "datadog,tracecontext", + extractFirst: false, + }, + { + name: "multi header - extractFirst", + extractEnv: "datadog,tracecontext", + extractFirst: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(headerPropagationStyleExtract, tt.extractEnv) + if tt.extractFirst { + t.Setenv("DD_TRACE_PROPAGATION_EXTRACT_FIRST", "true") + } + tracer := newTracer() + defer tracer.Stop() + ctx, err := tracer.Extract(TextMapCarrier{}) + assert.Equal(t, ErrSpanContextNotFound, err) + assert.Nil(t, ctx) + }) + } +} + func BenchmarkInjectDatadog(b *testing.B) { b.Setenv(headerPropagationStyleInject, "datadog") tracer := newTracer() From b143b60384364f6411395d99029ee7679473b2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Arg=C3=BCello?= Date: Mon, 3 Feb 2025 10:37:49 +0100 Subject: [PATCH 08/11] contrib/gorm.io/gorm.v1: fix orchestrion aspect (#3121) --- contrib/gorm.io/gorm.v1/orchestrion.yml | 16 +++++++++++++++- internal/orchestrion/_integration/gorm/gorm.go | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/contrib/gorm.io/gorm.v1/orchestrion.yml b/contrib/gorm.io/gorm.v1/orchestrion.yml index bb0eac6e12..c1ab640b2b 100644 --- a/contrib/gorm.io/gorm.v1/orchestrion.yml +++ b/contrib/gorm.io/gorm.v1/orchestrion.yml @@ -13,4 +13,18 @@ aspects: join-point: function-call: gorm.io/gorm.Open advice: - - replace-function: gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1.Open + - wrap-expression: + imports: + gorm: gorm.io/gorm + gormtrace: gopkg.in/DataDog/dd-trace-go.v1/contrib/gorm.io/gorm.v1 + template: |- + func() (*gorm.DB, error) { + db, err := {{ . }} + if err != nil { + return nil, err + } + if err := db.Use(gormtrace.NewTracePlugin()); err != nil { + return nil, err + } + return db, nil + }() diff --git a/internal/orchestrion/_integration/gorm/gorm.go b/internal/orchestrion/_integration/gorm/gorm.go index 0b3020f95c..6601824a87 100644 --- a/internal/orchestrion/_integration/gorm/gorm.go +++ b/internal/orchestrion/_integration/gorm/gorm.go @@ -23,7 +23,7 @@ type TestCase struct { func (tc *TestCase) Setup(_ context.Context, t *testing.T) { var err error - tc.DB, err = gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) + tc.DB, err = gorm.Open(sqlite.Open("file::memory:")) require.NoError(t, err) require.NoError(t, tc.DB.AutoMigrate(&Note{})) @@ -63,6 +63,21 @@ func (*TestCase) ExpectedTraces() trace.Traces { Meta: map[string]string{ "component": "gorm.io/gorm.v1", }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "resource": "SELECT * FROM `notes` WHERE user_id = ? AND `notes`.`deleted_at` IS NULL ORDER BY `notes`.`id` LIMIT 1", + "type": "sql", + "name": "sqlite3.query", + "service": "sqlite3.db", + }, + Meta: map[string]string{ + "component": "database/sql", + "span.kind": "client", + "db.system": "other_sql", + }, + }, + }, }, }, }, From 0a41ffdc54024b6fc4f7e88fc815b7ae9b8e6559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20L=C3=B3pez=20de=20Heredia?= Date: Mon, 3 Feb 2025 12:42:43 +0100 Subject: [PATCH 09/11] Fix peer tags configuration (#3132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dario Castañé --- ddtrace/tracer/option.go | 1 + ddtrace/tracer/option_test.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 4982e5cdaf..a9fedd5044 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -774,6 +774,7 @@ func loadAgentFeatures(agentDisabled bool, agentURL *url.URL, httpClient *http.C features.DropP0s = info.ClientDropP0s features.StatsdPort = info.Config.StatsdPort features.metaStructAvailable = info.SpanMetaStruct + features.peerTags = info.PeerTags for _, endpoint := range info.Endpoints { switch endpoint { case "/v0.6/stats": diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 8662bed43b..9045400905 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -224,7 +224,7 @@ func TestLoadAgentFeatures(t *testing.T) { t.Run("OK", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte(`{"endpoints":["/v0.6/stats"],"feature_flags":["a","b"],"client_drop_p0s":true,"config": {"statsd_port":8999}}`)) + w.Write([]byte(`{"endpoints":["/v0.6/stats"],"feature_flags":["a","b"],"client_drop_p0s":true,"peer_tags":["peer.hostname"],"config": {"statsd_port":8999}}`)) })) defer srv.Close() cfg := newConfig(WithAgentAddr(strings.TrimPrefix(srv.URL, "http://")), WithAgentTimeout(2)) @@ -237,6 +237,7 @@ func TestLoadAgentFeatures(t *testing.T) { assert.True(t, cfg.agent.Stats) assert.True(t, cfg.agent.HasFlag("a")) assert.True(t, cfg.agent.HasFlag("b")) + assert.EqualValues(t, cfg.agent.peerTags, []string{"peer.hostname"}) }) t.Run("discovery", func(t *testing.T) { From dbfb8f2354151b7562ecc24a6a6cf81bbabb82cc Mon Sep 17 00:00:00 2001 From: Mikayla Toffler <46911781+mtoffl01@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:09:46 -0500 Subject: [PATCH 10/11] contrib/database/sql: Close DB Stats goroutine on db.Close() (#3025) --- contrib/database/sql/metrics.go | 33 +++++++++++++++++----------- contrib/database/sql/metrics_test.go | 23 +++++++++++++++++++ contrib/database/sql/sql.go | 11 +++++++++- contrib/database/sql/sql_test.go | 8 ++++++- contrib/jackc/pgx.v5/metrics.go | 1 + 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/contrib/database/sql/metrics.go b/contrib/database/sql/metrics.go index d8ff4ed266..5d662ec9c6 100644 --- a/contrib/database/sql/metrics.go +++ b/contrib/database/sql/metrics.go @@ -33,20 +33,27 @@ var interval = 10 * time.Second // pollDBStats calls (*DB).Stats on the db at a predetermined interval. It pushes the DBStats off to the statsd client. // the caller should always ensure that db & statsd are non-nil -func pollDBStats(statsd internal.StatsdClient, db *sql.DB) { +func pollDBStats(statsd internal.StatsdClient, db *sql.DB, stop chan struct{}) { log.Debug("DB stats will be gathered and sent every %v.", interval) - for range time.NewTicker(interval).C { - log.Debug("Reporting DB.Stats metrics...") - stat := db.Stats() - statsd.Gauge(MaxOpenConnections, float64(stat.MaxOpenConnections), []string{}, 1) - statsd.Gauge(OpenConnections, float64(stat.OpenConnections), []string{}, 1) - statsd.Gauge(InUse, float64(stat.InUse), []string{}, 1) - statsd.Gauge(Idle, float64(stat.Idle), []string{}, 1) - statsd.Gauge(WaitCount, float64(stat.WaitCount), []string{}, 1) - statsd.Timing(WaitDuration, stat.WaitDuration, []string{}, 1) - statsd.Gauge(MaxIdleClosed, float64(stat.MaxIdleClosed), []string{}, 1) - statsd.Gauge(MaxIdleTimeClosed, float64(stat.MaxIdleTimeClosed), []string{}, 1) - statsd.Gauge(MaxLifetimeClosed, float64(stat.MaxLifetimeClosed), []string{}, 1) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + log.Debug("Reporting DB.Stats metrics...") + stat := db.Stats() + statsd.Gauge(MaxOpenConnections, float64(stat.MaxOpenConnections), []string{}, 1) + statsd.Gauge(OpenConnections, float64(stat.OpenConnections), []string{}, 1) + statsd.Gauge(InUse, float64(stat.InUse), []string{}, 1) + statsd.Gauge(Idle, float64(stat.Idle), []string{}, 1) + statsd.Gauge(WaitCount, float64(stat.WaitCount), []string{}, 1) + statsd.Timing(WaitDuration, stat.WaitDuration, []string{}, 1) + statsd.Gauge(MaxIdleClosed, float64(stat.MaxIdleClosed), []string{}, 1) + statsd.Gauge(MaxIdleTimeClosed, float64(stat.MaxIdleTimeClosed), []string{}, 1) + statsd.Gauge(MaxLifetimeClosed, float64(stat.MaxLifetimeClosed), []string{}, 1) + case <-stop: + return + } } } diff --git a/contrib/database/sql/metrics_test.go b/contrib/database/sql/metrics_test.go index e68e968229..fdc69fe6f7 100644 --- a/contrib/database/sql/metrics_test.go +++ b/contrib/database/sql/metrics_test.go @@ -6,9 +6,13 @@ package sql import ( + "sync" "testing" + "github.com/DataDog/datadog-go/v5/statsd" + "github.com/lib/pq" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" ) @@ -64,3 +68,22 @@ func TestStatsTags(t *testing.T) { }) resetGlobalConfig() } + +func TestPollDBStatsStop(t *testing.T) { + driverName := "postgres" + Register(driverName, &pq.Driver{}, WithServiceName("postgres-test"), WithAnalyticsRate(0.2)) + defer unregister(driverName) + db, err := Open(driverName, "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + require.NoError(t, err) + defer db.Close() + + var wg sync.WaitGroup + stop := make(chan struct{}) + wg.Add(1) + go func() { + defer wg.Done() + pollDBStats(&statsd.NoOpClientDirect{}, db, stop) + }() + close(stop) + wg.Wait() +} diff --git a/contrib/database/sql/sql.go b/contrib/database/sql/sql.go index b26318d0d3..e20601a41d 100644 --- a/contrib/database/sql/sql.go +++ b/contrib/database/sql/sql.go @@ -139,6 +139,7 @@ type tracedConnector struct { connector driver.Connector driverName string cfg *config + dbClose chan struct{} } func (t *tracedConnector) Connect(ctx context.Context) (driver.Conn, error) { @@ -171,6 +172,13 @@ func (t *tracedConnector) Driver() driver.Driver { return t.connector.Driver() } +// Close closes the dbClose channel +// This method will be invoked when DB.Close() is called, which we expect to occur only once: https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/database/sql/sql.go;l=918-950 +func (t *tracedConnector) Close() error { + close(t.dbClose) + return nil +} + // from Go stdlib implementation of sql.Open type dsnConnector struct { dsn string @@ -208,10 +216,11 @@ func OpenDB(c driver.Connector, opts ...Option) *sql.DB { connector: c, driverName: driverName, cfg: cfg, + dbClose: make(chan struct{}), } db := sql.OpenDB(tc) if cfg.dbStats && cfg.statsdClient != nil { - go pollDBStats(cfg.statsdClient, db) + go pollDBStats(cfg.statsdClient, db, tc.dbClose) } return db } diff --git a/contrib/database/sql/sql_test.go b/contrib/database/sql/sql_test.go index 5b50b7effc..e4d587b8ea 100644 --- a/contrib/database/sql/sql_test.go +++ b/contrib/database/sql/sql_test.go @@ -281,12 +281,13 @@ func TestOpenOptions(t *testing.T) { var tg statsdtest.TestStatsdClient Register(driverName, &pq.Driver{}) defer unregister(driverName) - _, err := Open(driverName, dsn, withStatsdClient(&tg), WithDBStats()) + db, err := Open(driverName, dsn, withStatsdClient(&tg), WithDBStats()) require.NoError(t, err) // The polling interval has been reduced to 500ms for the sake of this test, so at least one round of `pollDBStats` should be complete in 1s deadline := time.Now().Add(1 * time.Second) wantStats := []string{MaxOpenConnections, OpenConnections, InUse, Idle, WaitCount, WaitDuration, MaxIdleClosed, MaxIdleTimeClosed, MaxLifetimeClosed} + var calls1 []string for { if time.Now().After(deadline) { t.Fatalf("Stats not collected in expected interval of %v", interval) @@ -300,11 +301,16 @@ func TestOpenOptions(t *testing.T) { } } // all expected stats have been collected; exit out of loop, test should pass + calls1 = calls break } // not all stats have been collected yet, try again in 50ms time.Sleep(50 * time.Millisecond) } + // Close DB and assert the no further stats have been collected; db.Close should stop the pollDBStats goroutine. + db.Close() + time.Sleep(50 * time.Millisecond) + assert.Equal(t, calls1, tg.CallNames()) }) } diff --git a/contrib/jackc/pgx.v5/metrics.go b/contrib/jackc/pgx.v5/metrics.go index eb94c50bfc..9b84a4553f 100644 --- a/contrib/jackc/pgx.v5/metrics.go +++ b/contrib/jackc/pgx.v5/metrics.go @@ -35,6 +35,7 @@ var interval = 10 * time.Second // pollPoolStats calls (*pgxpool).Stats on the pool at a predetermined interval. It pushes the pool Stats off to the statsd client. func pollPoolStats(statsd internal.StatsdClient, pool *pgxpool.Pool) { + // TODO: Create stop condition for pgx on db.Close log.Debug("contrib/jackc/pgx.v5: Traced pool connection found: Pool stats will be gathered and sent every %v.", interval) for range time.NewTicker(interval).C { log.Debug("contrib/jackc/pgx.v5: Reporting pgxpool.Stat metrics...") From 998add0db80b859e3ec955d63240e7c2e7661f0c Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Mon, 3 Feb 2025 23:47:53 +0100 Subject: [PATCH 11/11] (feat) internal/civisibility: add Known Tests feature and refactor EFD logic (#3139) --- internal/civisibility/constants/test_tags.go | 3 + .../integrations/civisibility_features.go | 27 +-- .../coverage/coverage_writer_test.go | 6 +- .../integrations/gotesting/instrumentation.go | 175 +++++++++--------- .../gotesting/instrumentation_orchestrion.go | 15 ++ .../gotesting/testcontroller_test.go | 52 ++++-- .../integrations/gotesting/testing.go | 59 +++++- internal/civisibility/utils/net/client.go | 2 +- internal/civisibility/utils/net/efd_api.go | 116 ------------ .../civisibility/utils/net/known_tests_api.go | 116 ++++++++++++ ...fd_api_test.go => known_tests_api_test.go} | 28 +-- .../civisibility/utils/net/settings_api.go | 1 + .../utils/telemetry/telemetry_count.go | 12 +- .../utils/telemetry/telemetry_distribution.go | 18 +- 14 files changed, 367 insertions(+), 263 deletions(-) delete mode 100644 internal/civisibility/utils/net/efd_api.go create mode 100644 internal/civisibility/utils/net/known_tests_api.go rename internal/civisibility/utils/net/{efd_api_test.go => known_tests_api_test.go} (77%) diff --git a/internal/civisibility/constants/test_tags.go b/internal/civisibility/constants/test_tags.go index d3b30d461b..98c4bd9052 100644 --- a/internal/civisibility/constants/test_tags.go +++ b/internal/civisibility/constants/test_tags.go @@ -78,6 +78,9 @@ const ( // This constant is used to tag test events that are part of a retry execution TestIsRetry = "test.is_retry" + // TestRetryReason indicates the reason for retrying the test + TestRetryReason = "test.retry_reason" + // TestEarlyFlakeDetectionRetryAborted indicates a retry abort reason by the early flake detection feature TestEarlyFlakeDetectionRetryAborted = "test.early_flake.abort_reason" diff --git a/internal/civisibility/integrations/civisibility_features.go b/internal/civisibility/integrations/civisibility_features.go index 6b82bccf72..1155624433 100644 --- a/internal/civisibility/integrations/civisibility_features.go +++ b/internal/civisibility/integrations/civisibility_features.go @@ -51,8 +51,8 @@ var ( // ciVisibilitySettings contains the CI Visibility settings for this session ciVisibilitySettings net.SettingsResponseData - // ciVisibilityEarlyFlakyDetectionSettings contains the CI Visibility Early Flake Detection data for this session - ciVisibilityEarlyFlakyDetectionSettings net.EfdResponseData + // ciVisibilityKnownTests contains the CI Visibility Known Tests data for this session + ciVisibilityKnownTests net.KnownTestsResponseData // ciVisibilityFlakyRetriesSettings contains the CI Visibility Flaky Retries settings for this session ciVisibilityFlakyRetriesSettings FlakyRetriesSetting @@ -121,15 +121,20 @@ func ensureAdditionalFeaturesInitialization(serviceName string) { return } - // if early flake detection is enabled then we run the early flake detection request - if ciVisibilitySettings.EarlyFlakeDetection.Enabled { - ciEfdData, err := ciVisibilityClient.GetEarlyFlakeDetectionData() + // if early flake detection is enabled then we run the known tests request + if ciVisibilitySettings.KnownTestsEnabled { + ciEfdData, err := ciVisibilityClient.GetKnownTests() if err != nil { - log.Error("civisibility: error getting CI visibility early flake detection data: %v", err) + log.Error("civisibility: error getting CI visibility known tests data: %v", err) } else if ciEfdData != nil { - ciVisibilityEarlyFlakyDetectionSettings = *ciEfdData - log.Debug("civisibility: early flake detection data loaded.") + ciVisibilityKnownTests = *ciEfdData + log.Debug("civisibility: known tests data loaded.") } + } else { + // "known_tests_enabled" parameter works as a kill-switch for EFD, so if “known_tests_enabled” is false it + // will disable EFD even if “early_flake_detection.enabled” is set to true (which should not happen normally, + // the backend should disable both of them in that case) + ciVisibilitySettings.EarlyFlakeDetection.Enabled = false } // if flaky test retries is enabled then let's load the flaky retries settings @@ -172,11 +177,11 @@ func GetSettings() *net.SettingsResponseData { return &ciVisibilitySettings } -// GetEarlyFlakeDetectionSettings gets the early flake detection known tests data -func GetEarlyFlakeDetectionSettings() *net.EfdResponseData { +// GetKnownTests gets the known tests data +func GetKnownTests() *net.KnownTestsResponseData { // call to ensure the additional features initialization is completed (service name can be null here) ensureAdditionalFeaturesInitialization("") - return &ciVisibilityEarlyFlakyDetectionSettings + return &ciVisibilityKnownTests } // GetFlakyRetriesSettings gets the flaky retries settings diff --git a/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go b/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go index e3c59cea8f..657da02f88 100644 --- a/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go +++ b/internal/civisibility/integrations/gotesting/coverage/coverage_writer_test.go @@ -73,7 +73,7 @@ type MockClient struct { SendCoveragePayloadFunc func(ciTestCovPayload io.Reader) error SendCoveragePayloadWithFormatFunc func(ciTestCovPayload io.Reader, format string) error GetSettingsFunc func() (*net.SettingsResponseData, error) - GetEarlyFlakeDetectionDataFunc func() (*net.EfdResponseData, error) + GetKnownTestsFunc func() (*net.KnownTestsResponseData, error) GetCommitsFunc func(localCommits []string) ([]string, error) SendPackFilesFunc func(commitSha string, packFiles []string) (bytes int64, err error) GetSkippableTestsFunc func() (correlationId string, skippables map[string]map[string][]net.SkippableResponseDataAttributes, err error) @@ -91,8 +91,8 @@ func (m *MockClient) GetSettings() (*net.SettingsResponseData, error) { return m.GetSettingsFunc() } -func (m *MockClient) GetEarlyFlakeDetectionData() (*net.EfdResponseData, error) { - return m.GetEarlyFlakeDetectionDataFunc() +func (m *MockClient) GetKnownTests() (*net.KnownTestsResponseData, error) { + return m.GetKnownTestsFunc() } func (m *MockClient) GetCommits(localCommits []string) ([]string, error) { diff --git a/internal/civisibility/integrations/gotesting/instrumentation.go b/internal/civisibility/integrations/gotesting/instrumentation.go index 1c3be06223..45154a3417 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation.go +++ b/internal/civisibility/integrations/gotesting/instrumentation.go @@ -9,7 +9,6 @@ import ( "fmt" "reflect" "runtime" - "slices" "sync" "sync/atomic" "testing" @@ -36,7 +35,9 @@ type ( panicData any // panic data recovered from an internal test execution when using an additional feature wrapper panicStacktrace string // stacktrace from the panic recovered from an internal test isARetry bool // flag to tag if a current test execution is a retry - isANewTest bool // flag to tag if a current test execution is part of a new test (EFD not known test) + isANewTest bool // flag to tag if a current test execution is part of a new test + isEFDExecution bool // flag to tag if a current test execution is part of an EFD execution + isATRExecution bool // flag to tag if a current test execution is part of an ATR execution hasAdditionalFeatureWrapper bool // flag to check if the current execution is part of an additional feature wrapper } @@ -234,7 +235,10 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(* } } }, - execMetaAdjust: nil, // No execMetaAdjust needed + execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { + // Set the flag ATR execution to true + execMeta.isATRExecution = true + }, }) }, true } @@ -243,95 +247,82 @@ func applyFlakyTestRetriesAdditionalFeature(targetFunc func(*testing.T)) (func(* // applyEarlyFlakeDetectionAdditionalFeature applies the early flake detection feature as a wrapper of a func(*testing.T) func applyEarlyFlakeDetectionAdditionalFeature(testInfo *commonInfo, targetFunc func(*testing.T), settings *net.SettingsResponseData) (func(*testing.T), bool) { - earlyFlakeDetectionData := integrations.GetEarlyFlakeDetectionSettings() - if earlyFlakeDetectionData != nil && - len(earlyFlakeDetectionData.Tests) > 0 { - - // Define is a known test flag - isAKnownTest := false - - // Check if the test is a known test or a new one - if knownSuites, ok := earlyFlakeDetectionData.Tests[testInfo.moduleName]; ok { - if knownTests, ok := knownSuites[testInfo.suiteName]; ok { - if slices.Contains(knownTests, testInfo.testName) { - isAKnownTest = true - } - } - } + isKnown, hasKnownData := isKnownTest(testInfo) + if !hasKnownData || isKnown { + return targetFunc, false + } - // If it's a new test, then we apply the EFD wrapper - if !isAKnownTest { - return func(t *testing.T) { - var testPassCount, testSkipCount, testFailCount int - - runTestWithRetry(&runTestWithRetryOptions{ - targetFunc: targetFunc, - t: t, - initialRetryCount: 0, - adjustRetryCount: func(duration time.Duration) int64 { - slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries - durationSecs := duration.Seconds() - if durationSecs < 5 { - return int64(slowTestRetriesSettings.FiveS) - } else if durationSecs < 10 { - return int64(slowTestRetriesSettings.TenS) - } else if durationSecs < 30 { - return int64(slowTestRetriesSettings.ThirtyS) - } else if duration.Minutes() < 5 { - return int64(slowTestRetriesSettings.FiveM) - } - return 0 - }, - shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool { - return remainingRetries >= 0 - }, - perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) { - // Collect test results - if ptrToLocalT.Failed() { - testFailCount++ - } else if ptrToLocalT.Skipped() { - testSkipCount++ - } else { - testPassCount++ - } - }, - onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) { - // Update test status based on collected counts - tCommonPrivates := getTestPrivateFields(t) - if tCommonPrivates == nil { - panic("getting test private fields failed") - } - status := "passed" - if testPassCount == 0 { - if testSkipCount > 0 { - status = "skipped" - tCommonPrivates.SetSkipped(true) - } - if testFailCount > 0 { - status = "failed" - tCommonPrivates.SetFailed(true) - tParentCommonPrivates := getTestParentPrivateFields(t) - if tParentCommonPrivates == nil { - panic("getting test parent private fields failed") - } - tParentCommonPrivates.SetFailed(true) - } + // If it's a new test, then we apply the EFD wrapper + return func(t *testing.T) { + var testPassCount, testSkipCount, testFailCount int + + runTestWithRetry(&runTestWithRetryOptions{ + targetFunc: targetFunc, + t: t, + initialRetryCount: 0, + adjustRetryCount: func(duration time.Duration) int64 { + slowTestRetriesSettings := settings.EarlyFlakeDetection.SlowTestRetries + durationSecs := duration.Seconds() + if durationSecs < 5 { + return int64(slowTestRetriesSettings.FiveS) + } else if durationSecs < 10 { + return int64(slowTestRetriesSettings.TenS) + } else if durationSecs < 30 { + return int64(slowTestRetriesSettings.ThirtyS) + } else if duration.Minutes() < 5 { + return int64(slowTestRetriesSettings.FiveM) + } + return 0 + }, + shouldRetry: func(ptrToLocalT *testing.T, executionIndex int, remainingRetries int64) bool { + return remainingRetries >= 0 + }, + perExecution: func(ptrToLocalT *testing.T, executionIndex int, duration time.Duration) { + // Collect test results + if ptrToLocalT.Failed() { + testFailCount++ + } else if ptrToLocalT.Skipped() { + testSkipCount++ + } else { + testPassCount++ + } + }, + onRetryEnd: func(t *testing.T, executionIndex int, lastPtrToLocalT *testing.T) { + // Update test status based on collected counts + tCommonPrivates := getTestPrivateFields(t) + if tCommonPrivates == nil { + panic("getting test private fields failed") + } + status := "passed" + if testPassCount == 0 { + if testSkipCount > 0 { + status = "skipped" + tCommonPrivates.SetSkipped(true) + } + if testFailCount > 0 { + status = "failed" + tCommonPrivates.SetFailed(true) + tParentCommonPrivates := getTestParentPrivateFields(t) + if tParentCommonPrivates == nil { + panic("getting test parent private fields failed") } + tParentCommonPrivates.SetFailed(true) + } + } - // Print summary after retries - if executionIndex > 0 { - fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex) - } - }, - execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { - // Set the flag new test to true - execMeta.isANewTest = true - }, - }) - }, true - } - } - return targetFunc, false + // Print summary after retries + if executionIndex > 0 { + fmt.Printf(" [ %v after %v retries by Datadog's early flake detection ]\n", status, executionIndex) + } + }, + execMetaAdjust: func(execMeta *testExecutionMetadata, executionIndex int) { + // Set the flag new test to true + execMeta.isANewTest = true + // Set the flag EFD execution to true + execMeta.isEFDExecution = true + }, + }) + }, true } // runTestWithRetry encapsulates the common retry logic for test functions. @@ -386,6 +377,12 @@ func runTestWithRetry(options *runTestWithRetryOptions) { if originalExecMeta.isARetry { execMeta.isARetry = true } + if originalExecMeta.isEFDExecution { + execMeta.isEFDExecution = true + } + if originalExecMeta.isATRExecution { + execMeta.isATRExecution = true + } } // If we are in a retry execution, set the `isARetry` flag diff --git a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go index b87a4b9c13..c86b361214 100644 --- a/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go +++ b/internal/civisibility/integrations/gotesting/instrumentation_orchestrion.go @@ -170,6 +170,12 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { if parentExecMeta.isARetry { execMeta.isARetry = true } + if parentExecMeta.isEFDExecution { + execMeta.isEFDExecution = true + } + if parentExecMeta.isATRExecution { + execMeta.isATRExecution = true + } } } @@ -186,6 +192,15 @@ func instrumentTestingTFunc(f func(*testing.T)) func(*testing.T) { if execMeta.isARetry { // Set the retry tag test.SetTag(constants.TestIsRetry, "true") + + // If the execution is an EFD execution we tag the test event reason + if execMeta.isEFDExecution { + // Set the EFD as the retry reason + test.SetTag(constants.TestRetryReason, "efd") + } else if execMeta.isATRExecution { + // Set the ATR as the retry reason + test.SetTag(constants.TestRetryReason, "atr") + } } defer func() { diff --git a/internal/civisibility/integrations/gotesting/testcontroller_test.go b/internal/civisibility/integrations/gotesting/testcontroller_test.go index 1ab6347f84..7a5dc25fbb 100644 --- a/internal/civisibility/integrations/gotesting/testcontroller_test.go +++ b/internal/civisibility/integrations/gotesting/testcontroller_test.go @@ -74,7 +74,19 @@ func TestMain(m *testing.M) { func runFlakyTestRetriesTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, false, nil, false, nil) + server := setUpHttpServer(true, true, false, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ + "reflections_test.go": []string{ + "TestGetFieldPointerFrom", + "TestGetInternalTestArray", + "TestGetInternalBenchmarkArray", + "TestCommonPrivateFields_AddLevel", + "TestGetBenchmarkPrivateFields", + }, + }, + }, + }, false, nil) defer server.Close() // set a custom retry count @@ -137,6 +149,13 @@ func runFlakyTestRetriesTests(m *testing.M) { // check spans by tag checkSpansByTagName(finishedSpans, constants.TestIsRetry, 6) + trrSpan := checkSpansByTagName(finishedSpans, constants.TestRetryReason, 6)[0] + if trrSpan.Tag(constants.TestRetryReason) != "atr" { + panic(fmt.Sprintf("expected retry reason to be %s, got %s", "atr", trrSpan.Tag(constants.TestRetryReason))) + } + + // check the test is new tag + checkSpansByTagName(finishedSpans, constants.TestIsNew, 22) // check spans by type checkSpansByType(finishedSpans, @@ -153,9 +172,9 @@ func runFlakyTestRetriesTests(m *testing.M) { func runEarlyFlakyTestDetectionTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(false, true, &net.EfdResponseData{ - Tests: net.EfdResponseDataModules{ - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.EfdResponseDataSuites{ + server := setUpHttpServer(false, true, true, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ "reflections_test.go": []string{ "TestGetFieldPointerFrom", "TestGetInternalTestArray", @@ -227,6 +246,10 @@ func runEarlyFlakyTestDetectionTests(m *testing.M) { // check spans by tag checkSpansByTagName(finishedSpans, constants.TestIsNew, 176) checkSpansByTagName(finishedSpans, constants.TestIsRetry, 160) + trrSpan := checkSpansByTagName(finishedSpans, constants.TestRetryReason, 160)[0] + if trrSpan.Tag(constants.TestRetryReason) != "efd" { + panic(fmt.Sprintf("expected retry reason to be %s, got %s", "efd", trrSpan.Tag(constants.TestRetryReason))) + } // check spans by type checkSpansByType(finishedSpans, @@ -243,9 +266,9 @@ func runEarlyFlakyTestDetectionTests(m *testing.M) { func runFlakyTestRetriesWithEarlyFlakyTestDetectionTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, true, &net.EfdResponseData{ - Tests: net.EfdResponseDataModules{ - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.EfdResponseDataSuites{ + server := setUpHttpServer(true, true, true, &net.KnownTestsResponseData{ + Tests: net.KnownTestsResponseDataModules{ + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/integrations/gotesting": net.KnownTestsResponseDataSuites{ "reflections_test.go": []string{ "TestGetFieldPointerFrom", "TestGetInternalTestArray", @@ -355,7 +378,7 @@ func runFlakyTestRetriesWithEarlyFlakyTestDetectionTests(m *testing.M) { func runIntelligentTestRunnerTests(m *testing.M) { // mock the settings api to enable automatic test retries - server := setUpHttpServer(true, false, nil, true, []net.SkippableResponseDataAttributes{ + server := setUpHttpServer(true, true, false, nil, true, []net.SkippableResponseDataAttributes{ { Suite: "testing_test.go", Name: "TestMyTest01", @@ -569,8 +592,10 @@ type ( ) func setUpHttpServer(flakyRetriesEnabled bool, - earlyFlakyDetectionEnabled bool, earlyFlakyDetectionData *net.EfdResponseData, + knownTestsEnabled bool, + earlyFlakyDetectionEnabled bool, earlyFlakyDetectionData *net.KnownTestsResponseData, itrEnabled bool, itrData []net.SkippableResponseDataAttributes) *httptest.Server { + enableKnownTests := knownTestsEnabled || earlyFlakyDetectionEnabled // mock the settings api to enable automatic test retries server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Printf("MockApi received request: %s\n", r.URL.Path) @@ -591,6 +616,7 @@ func setUpHttpServer(flakyRetriesEnabled bool, FlakyTestRetriesEnabled: flakyRetriesEnabled, ItrEnabled: itrEnabled, TestsSkipping: itrEnabled, + KnownTestsEnabled: enableKnownTests, } response.Data.Attributes.EarlyFlakeDetection.Enabled = earlyFlakyDetectionEnabled response.Data.Attributes.EarlyFlakeDetection.SlowTestRetries.FiveS = 10 @@ -600,13 +626,13 @@ func setUpHttpServer(flakyRetriesEnabled bool, fmt.Printf("MockApi sending response: %v\n", response) json.NewEncoder(w).Encode(&response) - } else if earlyFlakyDetectionEnabled && r.URL.Path == "/api/v2/ci/libraries/tests" { + } else if enableKnownTests && r.URL.Path == "/api/v2/ci/libraries/tests" { w.Header().Set("Content-Type", "application/json") response := struct { Data struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes net.EfdResponseData `json:"attributes"` + ID string `json:"id"` + Type string `json:"type"` + Attributes net.KnownTestsResponseData `json:"attributes"` } `json:"data,omitempty"` }{} diff --git a/internal/civisibility/integrations/gotesting/testing.go b/internal/civisibility/integrations/gotesting/testing.go index a23d6665d3..4ef0865fb0 100644 --- a/internal/civisibility/integrations/gotesting/testing.go +++ b/internal/civisibility/integrations/gotesting/testing.go @@ -9,6 +9,7 @@ import ( "fmt" "reflect" "runtime" + "slices" "sync/atomic" "testing" "time" @@ -168,6 +169,7 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { settings := integrations.GetSettings() coverageEnabled := settings.CodeCoverage testSkippedByITR := false + testIsNew := true // Check if the test is going to be skipped by ITR if settings.ItrEnabled && settings.TestsSkipping { @@ -182,6 +184,15 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { } } + // Check if the test is known + if settings.KnownTestsEnabled { + testIsKnown, testKnownDataOk := isKnownTest(&testInfo.commonInfo) + testIsNew = testKnownDataOk && !testIsKnown + } else { + // We don't mark any test as new if the feature is disabled + testIsNew = false + } + // Instrument the test function instrumentedFunc := func(t *testing.T) { // Set this func as a helper func of t @@ -204,7 +215,8 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { // Set the CI Visibility test to the execution metadata execMeta.test = test - // If the execution is for a new test we tag the test event from early flake detection + // If the execution is for a new test we tag the test event as new + execMeta.isANewTest = execMeta.isANewTest || testIsNew if execMeta.isANewTest { // Set the is new test tag test.SetTag(constants.TestIsNew, "true") @@ -214,6 +226,15 @@ func (ddm *M) executeInternalTest(testInfo *testingTInfo) func(*testing.T) { if execMeta.isARetry { // Set the retry tag test.SetTag(constants.TestIsRetry, "true") + + // If the execution is an EFD execution we tag the test event reason + if execMeta.isEFDExecution { + // Set the EFD as the retry reason + test.SetTag(constants.TestRetryReason, "efd") + } else if execMeta.isATRExecution { + // Set the ATR as the retry reason + test.SetTag(constants.TestRetryReason, "atr") + } } // Check if the test needs to be skipped by ITR @@ -385,6 +406,19 @@ func (ddm *M) instrumentInternalBenchmarks(internalBenchmarks *[]testing.Interna // executeInternalBenchmark wraps the original benchmark function to include CI visibility instrumentation. func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testing.B) { originalFunc := runtime.FuncForPC(reflect.Indirect(reflect.ValueOf(benchmarkInfo.originalFunc)).Pointer()) + + settings := integrations.GetSettings() + testIsNew := true + + // Check if the test is known + if settings.KnownTestsEnabled { + testIsKnown, testKnownDataOk := isKnownTest(&benchmarkInfo.commonInfo) + testIsNew = testKnownDataOk && !testIsKnown + } else { + // We don't mark any test as new if the feature is disabled + testIsNew = false + } + instrumentedInternalFunc := func(b *testing.B) { // decrement level @@ -399,6 +433,12 @@ func (ddm *M) executeInternalBenchmark(benchmarkInfo *testingBInfo) func(*testin test := suite.CreateTest(benchmarkInfo.testName, integrations.WithTestStartTime(startTime)) test.SetTestFunc(originalFunc) + // If the execution is for a new test we tag the test event as new + if testIsNew { + // Set the is new test tag + test.SetTag(constants.TestIsNew, "true") + } + // Run the original benchmark function. var iPfOfB *benchmarkPrivateFields var recoverFunc *func(r any) @@ -528,3 +568,20 @@ func checkModuleAndSuite(module integrations.TestModule, suite integrations.Test module.Close() } } + +// isKnownTest checks if a test is a known test or a new one +func isKnownTest(testInfo *commonInfo) (isKnown bool, hasKnownData bool) { + knownTestsData := integrations.GetKnownTests() + if knownTestsData != nil && len(knownTestsData.Tests) > 0 { + // Check if the test is a known test or a new one + if knownSuites, ok := knownTestsData.Tests[testInfo.moduleName]; ok { + if knownTests, ok := knownSuites[testInfo.suiteName]; ok { + return slices.Contains(knownTests, testInfo.testName), true + } + } + + return false, true + } + + return false, false +} diff --git a/internal/civisibility/utils/net/client.go b/internal/civisibility/utils/net/client.go index 23113a893d..3bcfcdd847 100644 --- a/internal/civisibility/utils/net/client.go +++ b/internal/civisibility/utils/net/client.go @@ -38,7 +38,7 @@ type ( // Client is an interface for sending requests to the Datadog backend. Client interface { GetSettings() (*SettingsResponseData, error) - GetEarlyFlakeDetectionData() (*EfdResponseData, error) + GetKnownTests() (*KnownTestsResponseData, error) GetCommits(localCommits []string) ([]string, error) SendPackFiles(commitSha string, packFiles []string) (bytes int64, err error) SendCoveragePayload(ciTestCovPayload io.Reader) error diff --git a/internal/civisibility/utils/net/efd_api.go b/internal/civisibility/utils/net/efd_api.go deleted file mode 100644 index 898e13603a..0000000000 --- a/internal/civisibility/utils/net/efd_api.go +++ /dev/null @@ -1,116 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2024 Datadog, Inc. - -package net - -import ( - "fmt" - "time" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" -) - -const ( - efdRequestType string = "ci_app_libraries_tests_request" - efdURLPath string = "api/v2/ci/libraries/tests" -) - -type ( - efdRequest struct { - Data efdRequestHeader `json:"data"` - } - - efdRequestHeader struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes EfdRequestData `json:"attributes"` - } - - EfdRequestData struct { - Service string `json:"service"` - Env string `json:"env"` - RepositoryURL string `json:"repository_url"` - Configurations testConfigurations `json:"configurations"` - } - - efdResponse struct { - Data struct { - ID string `json:"id"` - Type string `json:"type"` - Attributes EfdResponseData `json:"attributes"` - } `json:"data"` - } - - EfdResponseData struct { - Tests EfdResponseDataModules `json:"tests"` - } - - EfdResponseDataModules map[string]EfdResponseDataSuites - EfdResponseDataSuites map[string][]string -) - -func (c *client) GetEarlyFlakeDetectionData() (*EfdResponseData, error) { - if c.repositoryURL == "" || c.commitSha == "" { - return nil, fmt.Errorf("civisibility.GetEarlyFlakeDetectionData: repository URL and commit SHA are required") - } - - body := efdRequest{ - Data: efdRequestHeader{ - ID: c.id, - Type: efdRequestType, - Attributes: EfdRequestData{ - Service: c.serviceName, - Env: c.environment, - RepositoryURL: c.repositoryURL, - Configurations: c.testConfigurations, - }, - }, - } - - request := c.getPostRequestConfig(efdURLPath, body) - if request.Compressed { - telemetry.EarlyFlakeDetectionRequest(telemetry.CompressedRequestCompressedType) - } else { - telemetry.EarlyFlakeDetectionRequest(telemetry.UncompressedRequestCompressedType) - } - - startTime := time.Now() - response, err := c.handler.SendRequest(*request) - telemetry.EarlyFlakeDetectionRequestMs(float64(time.Since(startTime).Milliseconds())) - - if err != nil { - telemetry.EarlyFlakeDetectionRequestErrors(telemetry.NetworkErrorType) - return nil, fmt.Errorf("sending early flake detection request: %s", err.Error()) - } - - if response.StatusCode < 200 || response.StatusCode >= 300 { - telemetry.EarlyFlakeDetectionRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) - } - if response.Compressed { - telemetry.EarlyFlakeDetectionResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) - } else { - telemetry.EarlyFlakeDetectionResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) - } - - var responseObject efdResponse - err = response.Unmarshal(&responseObject) - if err != nil { - return nil, fmt.Errorf("unmarshalling early flake detection data response: %s", err.Error()) - } - - testCount := 0 - if responseObject.Data.Attributes.Tests != nil { - for _, suites := range responseObject.Data.Attributes.Tests { - if suites == nil { - continue - } - for _, tests := range suites { - testCount += len(tests) - } - } - } - telemetry.EarlyFlakeDetectionResponseTests(float64(testCount)) - return &responseObject.Data.Attributes, nil -} diff --git a/internal/civisibility/utils/net/known_tests_api.go b/internal/civisibility/utils/net/known_tests_api.go new file mode 100644 index 0000000000..ecaeb638a2 --- /dev/null +++ b/internal/civisibility/utils/net/known_tests_api.go @@ -0,0 +1,116 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package net + +import ( + "fmt" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/utils/telemetry" +) + +const ( + knownTestsRequestType string = "ci_app_libraries_tests_request" + knownTestsURLPath string = "api/v2/ci/libraries/tests" +) + +type ( + knownTestsRequest struct { + Data knownTestsRequestHeader `json:"data"` + } + + knownTestsRequestHeader struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes KnownTestsRequestData `json:"attributes"` + } + + KnownTestsRequestData struct { + Service string `json:"service"` + Env string `json:"env"` + RepositoryURL string `json:"repository_url"` + Configurations testConfigurations `json:"configurations"` + } + + knownTestsResponse struct { + Data struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes KnownTestsResponseData `json:"attributes"` + } `json:"data"` + } + + KnownTestsResponseData struct { + Tests KnownTestsResponseDataModules `json:"tests"` + } + + KnownTestsResponseDataModules map[string]KnownTestsResponseDataSuites + KnownTestsResponseDataSuites map[string][]string +) + +func (c *client) GetKnownTests() (*KnownTestsResponseData, error) { + if c.repositoryURL == "" || c.commitSha == "" { + return nil, fmt.Errorf("civisibility.GetKnownTests: repository URL and commit SHA are required") + } + + body := knownTestsRequest{ + Data: knownTestsRequestHeader{ + ID: c.id, + Type: knownTestsRequestType, + Attributes: KnownTestsRequestData{ + Service: c.serviceName, + Env: c.environment, + RepositoryURL: c.repositoryURL, + Configurations: c.testConfigurations, + }, + }, + } + + request := c.getPostRequestConfig(knownTestsURLPath, body) + if request.Compressed { + telemetry.KnownTestsRequest(telemetry.CompressedRequestCompressedType) + } else { + telemetry.KnownTestsRequest(telemetry.UncompressedRequestCompressedType) + } + + startTime := time.Now() + response, err := c.handler.SendRequest(*request) + telemetry.KnownTestsRequestMs(float64(time.Since(startTime).Milliseconds())) + + if err != nil { + telemetry.KnownTestsRequestErrors(telemetry.NetworkErrorType) + return nil, fmt.Errorf("sending known tests request: %s", err.Error()) + } + + if response.StatusCode < 200 || response.StatusCode >= 300 { + telemetry.KnownTestsRequestErrors(telemetry.GetErrorTypeFromStatusCode(response.StatusCode)) + } + if response.Compressed { + telemetry.KnownTestsResponseBytes(telemetry.CompressedResponseCompressedType, float64(len(response.Body))) + } else { + telemetry.KnownTestsResponseBytes(telemetry.UncompressedResponseCompressedType, float64(len(response.Body))) + } + + var responseObject knownTestsResponse + err = response.Unmarshal(&responseObject) + if err != nil { + return nil, fmt.Errorf("unmarshalling known tests response: %s", err.Error()) + } + + testCount := 0 + if responseObject.Data.Attributes.Tests != nil { + for _, suites := range responseObject.Data.Attributes.Tests { + if suites == nil { + continue + } + for _, tests := range suites { + testCount += len(tests) + } + } + } + telemetry.KnownTestsResponseTests(float64(testCount)) + return &responseObject.Data.Attributes, nil +} diff --git a/internal/civisibility/utils/net/efd_api_test.go b/internal/civisibility/utils/net/known_tests_api_test.go similarity index 77% rename from internal/civisibility/utils/net/efd_api_test.go rename to internal/civisibility/utils/net/known_tests_api_test.go index 93008d25ce..79c894883f 100644 --- a/internal/civisibility/utils/net/efd_api_test.go +++ b/internal/civisibility/utils/net/known_tests_api_test.go @@ -16,15 +16,15 @@ import ( "github.com/stretchr/testify/assert" ) -func TestEfdApiRequest(t *testing.T) { +func TestKnownTestsApiRequest(t *testing.T) { var c *client - expectedResponse := efdResponse{} + expectedResponse := knownTestsResponse{} expectedResponse.Data.Type = settingsRequestType - expectedResponse.Data.Attributes.Tests = EfdResponseDataModules{ - "MyModule1": EfdResponseDataSuites{ + expectedResponse.Data.Attributes.Tests = KnownTestsResponseDataModules{ + "MyModule1": KnownTestsResponseDataSuites{ "MySuite1": []string{"Test1", "Test2"}, }, - "MyModule2": EfdResponseDataSuites{ + "MyModule2": KnownTestsResponseDataSuites{ "MySuite2": []string{"Test3", "Test4"}, }, } @@ -37,11 +37,11 @@ func TestEfdApiRequest(t *testing.T) { } if r.Header.Get(HeaderContentType) == ContentTypeJSON { - var request efdRequest + var request knownTestsRequest json.Unmarshal(body, &request) assert.Equal(t, c.id, request.Data.ID) - assert.Equal(t, efdRequestType, request.Data.Type) - assert.Equal(t, efdURLPath, r.URL.Path[1:]) + assert.Equal(t, knownTestsRequestType, request.Data.Type) + assert.Equal(t, knownTestsURLPath, r.URL.Path[1:]) assert.Equal(t, c.environment, request.Data.Attributes.Env) assert.Equal(t, c.repositoryURL, request.Data.Attributes.RepositoryURL) assert.Equal(t, c.serviceName, request.Data.Attributes.Service) @@ -62,12 +62,12 @@ func TestEfdApiRequest(t *testing.T) { cInterface := NewClient() c = cInterface.(*client) - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, err) assert.Equal(t, expectedResponse.Data.Attributes, *efdData) } -func TestEfdApiRequestFailToUnmarshal(t *testing.T) { +func TestKnownTestsApiRequestFailToUnmarshal(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to read body", http.StatusBadRequest) })) @@ -80,13 +80,13 @@ func TestEfdApiRequestFailToUnmarshal(t *testing.T) { setCiVisibilityEnv(path, server.URL) cInterface := NewClient() - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, efdData) assert.NotNil(t, err) assert.Contains(t, err.Error(), "cannot unmarshal response") } -func TestEfdApiRequestFailToGet(t *testing.T) { +func TestKnownTestsApiRequestFailToGet(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "internal processing error", http.StatusInternalServerError) })) @@ -99,8 +99,8 @@ func TestEfdApiRequestFailToGet(t *testing.T) { setCiVisibilityEnv(path, server.URL) cInterface := NewClient() - efdData, err := cInterface.GetEarlyFlakeDetectionData() + efdData, err := cInterface.GetKnownTests() assert.Nil(t, efdData) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "sending early flake detection request") + assert.Contains(t, err.Error(), "sending known tests request") } diff --git a/internal/civisibility/utils/net/settings_api.go b/internal/civisibility/utils/net/settings_api.go index d0b927bb6d..205d06e737 100644 --- a/internal/civisibility/utils/net/settings_api.go +++ b/internal/civisibility/utils/net/settings_api.go @@ -62,6 +62,7 @@ type ( ItrEnabled bool `json:"itr_enabled"` RequireGit bool `json:"require_git"` TestsSkipping bool `json:"tests_skipping"` + KnownTestsEnabled bool `json:"known_tests_enabled"` } ) diff --git a/internal/civisibility/utils/telemetry/telemetry_count.go b/internal/civisibility/utils/telemetry/telemetry_count.go index 337f47f92d..0e1bfaa931 100644 --- a/internal/civisibility/utils/telemetry/telemetry_count.go +++ b/internal/civisibility/utils/telemetry/telemetry_count.go @@ -199,14 +199,14 @@ func CodeCoverageErrors() { telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.errors", 1.0, nil, true) } -// EarlyFlakeDetectionRequest the number of requests sent to the early flake detection endpoint, tagged by the request compressed type. -func EarlyFlakeDetectionRequest(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request", 1.0, removeEmptyStrings([]string{ +// KnownTestsRequest the number of requests sent to the known tests endpoint, tagged by the request compressed type. +func KnownTestsRequest(requestCompressed RequestCompressedType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request", 1.0, removeEmptyStrings([]string{ string(requestCompressed), }), true) } -// EarlyFlakeDetectionRequestErrors the number of requests sent to the early flake detection endpoint that errored, tagged by the error type. -func EarlyFlakeDetectionRequestErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "early_flake_detection.request_errors", 1.0, removeEmptyStrings(errorType), true) +// KnownTestsRequestErrors the number of requests sent to the known tests endpoint that errored, tagged by the error type. +func KnownTestsRequestErrors(errorType ErrorType) { + telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request_errors", 1.0, removeEmptyStrings(errorType), true) } diff --git a/internal/civisibility/utils/telemetry/telemetry_distribution.go b/internal/civisibility/utils/telemetry/telemetry_distribution.go index 3b4d8d54fe..20c5786eb0 100644 --- a/internal/civisibility/utils/telemetry/telemetry_distribution.go +++ b/internal/civisibility/utils/telemetry/telemetry_distribution.go @@ -86,19 +86,19 @@ func CodeCoverageFiles(value float64) { telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "code_coverage.files", value, nil, true) } -// EarlyFlakeDetectionRequestMs records the time it takes to get the response of the early flake detection endpoint request in ms by CI Visibility. -func EarlyFlakeDetectionRequestMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.request_ms", value, nil, true) +// KnownTestsRequestMs records the time it takes to get the response of the known tests endpoint request in ms by CI Visibility. +func KnownTestsRequestMs(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.request_ms", value, nil, true) } -// EarlyFlakeDetectionResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. -func EarlyFlakeDetectionResponseBytes(responseCompressedType ResponseCompressedType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_bytes", value, removeEmptyStrings([]string{ +// KnownTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. +func KnownTestsResponseBytes(responseCompressedType ResponseCompressedType, value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_bytes", value, removeEmptyStrings([]string{ (string)(responseCompressedType), }), true) } -// EarlyFlakeDetectionResponseTests records the number of tests in the response of the early flake detection endpoint by CI Visibility. -func EarlyFlakeDetectionResponseTests(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "early_flake_detection.response_tests", value, nil, true) +// KnownTestsResponseTests records the number of tests in the response of the known tests endpoint by CI Visibility. +func KnownTestsResponseTests(value float64) { + telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_tests", value, nil, true) }