diff --git a/.chloggen/datadogreceiver-span-obfuscate.yaml b/.chloggen/datadogreceiver-span-obfuscate.yaml new file mode 100644 index 000000000000..7348b5732eb2 --- /dev/null +++ b/.chloggen/datadogreceiver-span-obfuscate.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: datadogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement span obfuscation for Datadog receiver. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35593] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/receiver/datadogreceiver/README.md b/receiver/datadogreceiver/README.md index 32513a6d6d7a..6cefe99efbf3 100644 --- a/receiver/datadogreceiver/README.md +++ b/receiver/datadogreceiver/README.md @@ -29,6 +29,9 @@ receivers: datadog: endpoint: localhost:8126 read_timeout: 60s + traces: + obfuscation: + enabled: true exporters: debug: @@ -48,6 +51,10 @@ The read timeout of the HTTP Server Default: 60s +### Traces (Optional) +#### Obfuscation +The obfuscation configuration allows you to obfuscate and cleanup sensitive information in the trace data. The complete list of obfuscation rules and configuration can be found [here](common/config.go). + ### HTTP Service Config All config params here are valid as well diff --git a/receiver/datadogreceiver/common/config.go b/receiver/datadogreceiver/common/config.go new file mode 100644 index 000000000000..9936067c6975 --- /dev/null +++ b/receiver/datadogreceiver/common/config.go @@ -0,0 +1,188 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 language governing permissions and +// limitations under the License. + +package common // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" + +import ( + "time" + + "github.com/DataDog/datadog-agent/pkg/obfuscate" + "go.opentelemetry.io/collector/config/confighttp" +) + +func NewConfig() *Config { + return &Config{ + ServerConfig: confighttp.ServerConfig{ + Endpoint: "localhost:8126", + }, + ReadTimeout: 60 * time.Second, + Traces: TracesConfig{ + Obfuscation: ObfuscationConfig{}, + }, + } +} + +type Config struct { + confighttp.ServerConfig `mapstructure:",squash"` + // ReadTimeout of the http server + ReadTimeout time.Duration `mapstructure:"read_timeout"` + + // Traces holds tracing-related configurations + Traces TracesConfig `mapstructure:"traces"` +} + +// TracesConfig holds the configuration for the Datadog receiver's trace processor. +type TracesConfig struct { + // Obfuscation holds sensitive data obufscator's configuration. + Obfuscation ObfuscationConfig `mapstructure:"obfuscation"` +} + +// ObfuscationConfig holds the configuration for obfuscating sensitive data +// for various span types. +type ObfuscationConfig struct { + Enabled bool `mapstructure:"enabled"` + + // SQL holds the obfuscation configuration for SQL queries. + SQL SQLConfig `mapstructure:"sql"` + + // ES holds the obfuscation configuration for ElasticSearch bodies. + ES obfuscate.JSONConfig `mapstructure:"elasticsearch"` + + // OpenSearch holds the obfuscation configuration for OpenSearch bodies. + OpenSearch obfuscate.JSONConfig `mapstructure:"opensearch"` + + // Mongo holds the obfuscation configuration for MongoDB queries. + Mongo obfuscate.JSONConfig `mapstructure:"mongodb"` + + // SQLExecPlan holds the obfuscation configuration for SQL Exec Plans. This is strictly for safety related obfuscation, + // not normalization. Normalization of exec plans is configured in SQLExecPlanNormalize. + SQLExecPlan obfuscate.JSONConfig `mapstructure:"sql_exec_plan"` + + // SQLExecPlanNormalize holds the normalization configuration for SQL Exec Plans. + SQLExecPlanNormalize obfuscate.JSONConfig `mapstructure:"sql_exec_plan_normalize"` + + // HTTP holds the obfuscation settings for HTTP URLs. + HTTP obfuscate.HTTPConfig `mapstructure:"http"` + + // RemoveStackTraces specifies whether stack traces should be removed. + // More specifically "error.stack" tag values will be cleared. + RemoveStackTraces bool `mapstructure:"remove_stack_traces"` + + // Redis holds the configuration for obfuscating the "redis.raw_command" tag + // for spans of type "redis". + Redis obfuscate.RedisConfig `mapstructure:"redis"` + + // Memcached holds the configuration for obfuscating the "memcached.command" tag + // for spans of type "memcached". + Memcached obfuscate.MemcachedConfig `mapstructure:"memcached"` + + // CreditCards holds the configuration for obfuscating credit cards. + CreditCards obfuscate.CreditCardsConfig `mapstructure:"credit_cards"` +} + +// SQLConfig holds the config for obfuscating SQL. +type SQLConfig struct { + // DBMS identifies the type of database management system (e.g. MySQL, Postgres, and SQL Server). + // Valid values for this can be found at https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#connection-level-attributes + DBMS string `mapstructure:"dbms"` + + // TableNames specifies whether the obfuscator should also extract the table names that a query addresses, + // in addition to obfuscating. + TableNames bool `mapstructure:"table_names"` + + // CollectCommands specifies whether the obfuscator should extract and return commands as SQL metadata when obfuscating. + CollectCommands bool `mapstructure:"collect_commands"` + + // CollectComments specifies whether the obfuscator should extract and return comments as SQL metadata when obfuscating. + CollectComments bool `mapstructure:"collect_comments"` + + // CollectProcedures specifies whether the obfuscator should extract and return procedure names as SQL metadata when obfuscating. + CollectProcedures bool `mapstructure:"collect_procedures"` + + // ReplaceDigits specifies whether digits in table names and identifiers should be obfuscated. + ReplaceDigits bool `mapstructure:"replace_digits"` + + // KeepSQLAlias reports whether SQL aliases ("AS") should be truncated. + KeepSQLAlias bool `mapstructure:"keep_sql_alias"` + + // DollarQuotedFunc reports whether to treat "$func$" delimited dollar-quoted strings + // differently and not obfuscate them as a string. To read more about dollar quoted + // strings see: + // + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING + DollarQuotedFunc bool `mapstructure:"dollar_quoted_func"` + + // ObfuscationMode specifies the obfuscation mode to use for go-sqllexer pkg. + // When specified, obfuscator will attempt to use go-sqllexer pkg to obfuscate (and normalize) SQL queries. + // Valid values are "normalize_only", "obfuscate_only", "obfuscate_and_normalize" + ObfuscationMode obfuscate.ObfuscationMode `mapstructure:"obfuscation_mode"` + + // RemoveSpaceBetweenParentheses specifies whether to remove spaces between parentheses. + // By default, spaces are inserted between parentheses during normalization. + // This option is only valid when ObfuscationMode is "normalize_only" or "obfuscate_and_normalize". + RemoveSpaceBetweenParentheses bool `mapstructure:"remove_space_between_parentheses"` + + // KeepNull specifies whether to disable obfuscate NULL value with ?. + // This option is only valid when ObfuscationMode is "obfuscate_only" or "obfuscate_and_normalize". + KeepNull bool `mapstructure:"keep_null"` + + // KeepBoolean specifies whether to disable obfuscate boolean value with ?. + // This option is only valid when ObfuscationMode is "obfuscate_only" or "obfuscate_and_normalize". + KeepBoolean bool `mapstructure:"keep_boolean"` + + // KeepPositionalParameter specifies whether to disable obfuscate positional parameter with ?. + // This option is only valid when ObfuscationMode is "obfuscate_only" or "obfuscate_and_normalize". + KeepPositionalParameter bool `mapstructure:"keep_positional_parameter"` + + // KeepTrailingSemicolon specifies whether to keep trailing semicolon. + // By default, trailing semicolon is removed during normalization. + // This option is only valid when ObfuscationMode is "normalize_only" or "obfuscate_and_normalize". + KeepTrailingSemicolon bool `mapstructure:"keep_trailing_semicolon"` + + // KeepIdentifierQuotation specifies whether to keep identifier quotation, e.g. "my_table" or [my_table]. + // By default, identifier quotation is removed during normalization. + // This option is only valid when ObfuscationMode is "normalize_only" or "obfuscate_and_normalize". + KeepIdentifierQuotation bool `mapstructure:"keep_identifier_quotation"` +} + +// Export returns an obfuscate.Config matching o. +func (o *ObfuscationConfig) Export() obfuscate.Config { + if !o.Enabled { + return obfuscate.Config{} + } + + return obfuscate.Config{ + SQL: o.SQL.Export(), + ES: o.ES, + OpenSearch: o.OpenSearch, + Mongo: o.Mongo, + SQLExecPlan: o.SQLExecPlan, + SQLExecPlanNormalize: o.SQLExecPlanNormalize, + HTTP: o.HTTP, + Redis: o.Redis, + Memcached: o.Memcached, + CreditCard: o.CreditCards, + } +} + +// Export returns an obfuscate.Config matching o. +func (o *SQLConfig) Export() obfuscate.SQLConfig { + return obfuscate.SQLConfig{ + DBMS: o.DBMS, + TableNames: o.TableNames, + CollectCommands: o.CollectCommands, + CollectComments: o.CollectComments, + CollectProcedures: o.CollectProcedures, + ReplaceDigits: o.ReplaceDigits, + KeepSQLAlias: o.KeepSQLAlias, + DollarQuotedFunc: o.DollarQuotedFunc, + ObfuscationMode: o.ObfuscationMode, + KeepNull: o.KeepNull, + KeepBoolean: o.KeepBoolean, + KeepPositionalParameter: o.KeepPositionalParameter, + KeepTrailingSemicolon: o.KeepTrailingSemicolon, + KeepIdentifierQuotation: o.KeepIdentifierQuotation, + RemoveSpaceBetweenParentheses: o.RemoveSpaceBetweenParentheses, + } +} diff --git a/receiver/datadogreceiver/config.go b/receiver/datadogreceiver/config.go deleted file mode 100644 index 646309c99763..000000000000 --- a/receiver/datadogreceiver/config.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 language governing permissions and -// limitations under the License. - -package datadogreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver" - -import ( - "time" - - "go.opentelemetry.io/collector/config/confighttp" -) - -type Config struct { - confighttp.ServerConfig `mapstructure:",squash"` - // ReadTimeout of the http server - ReadTimeout time.Duration `mapstructure:"read_timeout"` -} diff --git a/receiver/datadogreceiver/config_test.go b/receiver/datadogreceiver/config_test.go deleted file mode 100644 index a67e9596d66b..000000000000 --- a/receiver/datadogreceiver/config_test.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package datadogreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver" - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCreateDefaultConfig(t *testing.T) { - factory := NewFactory() - cfg := factory.CreateDefaultConfig() - assert.NotNil(t, cfg, "failed to create default config") -} diff --git a/receiver/datadogreceiver/factory.go b/receiver/datadogreceiver/factory.go index 6403be658d3b..b0c528b9d07d 100644 --- a/receiver/datadogreceiver/factory.go +++ b/receiver/datadogreceiver/factory.go @@ -5,14 +5,13 @@ package datadogreceiver // import "github.com/open-telemetry/opentelemetry-colle import ( "context" - "time" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/receiver" "github.com/open-telemetry/opentelemetry-collector-contrib/internal/sharedcomponent" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/metadata" ) @@ -27,17 +26,12 @@ func NewFactory() receiver.Factory { } func createDefaultConfig() component.Config { - return &Config{ - ServerConfig: confighttp.ServerConfig{ - Endpoint: "localhost:8126", - }, - ReadTimeout: 60 * time.Second, - } + return common.NewConfig() } func createTracesReceiver(_ context.Context, params receiver.Settings, cfg component.Config, consumer consumer.Traces) (receiver.Traces, error) { var err error - rcfg := cfg.(*Config) + rcfg := cfg.(*common.Config) r := receivers.GetOrAdd(rcfg, func() (dd component.Component) { dd, err = newDataDogReceiver(rcfg, params) return dd @@ -52,7 +46,7 @@ func createTracesReceiver(_ context.Context, params receiver.Settings, cfg compo func createMetricsReceiver(_ context.Context, params receiver.Settings, cfg component.Config, consumer consumer.Metrics) (receiver.Metrics, error) { var err error - rcfg := cfg.(*Config) + rcfg := cfg.(*common.Config) r := receivers.GetOrAdd(cfg, func() (dd component.Component) { dd, err = newDataDogReceiver(rcfg, params) return dd diff --git a/receiver/datadogreceiver/factory_test.go b/receiver/datadogreceiver/factory_test.go index c911d2601e31..101daa013894 100644 --- a/receiver/datadogreceiver/factory_test.go +++ b/receiver/datadogreceiver/factory_test.go @@ -10,12 +10,14 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/consumer/consumertest" "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" ) func TestCreateTraces(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - cfg.(*Config).Endpoint = "http://localhost:0" + cfg.(*common.Config).Endpoint = "http://localhost:0" tReceiver, err := factory.CreateTraces(context.Background(), receivertest.NewNopSettings(), cfg, consumertest.NewNop()) assert.NoError(t, err) @@ -25,9 +27,15 @@ func TestCreateTraces(t *testing.T) { func TestCreateMetrics(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - cfg.(*Config).Endpoint = "http://localhost:0" + cfg.(*common.Config).Endpoint = "http://localhost:0" tReceiver, err := factory.CreateMetrics(context.Background(), receivertest.NewNopSettings(), cfg, consumertest.NewNop()) assert.NoError(t, err) assert.NotNil(t, tReceiver, "metrics receiver creation failed") } + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") +} diff --git a/receiver/datadogreceiver/internal/translator/traces_translator.go b/receiver/datadogreceiver/internal/translator/traces_translator.go index 59a70b012161..78a833685e3b 100644 --- a/receiver/datadogreceiver/internal/translator/traces_translator.go +++ b/receiver/datadogreceiver/internal/translator/traces_translator.go @@ -16,12 +16,15 @@ import ( "strings" "sync" + "github.com/DataDog/datadog-agent/pkg/obfuscate" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace" + "github.com/DataDog/datadog-agent/pkg/trace/traceutil" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/ptrace" semconv "go.opentelemetry.io/collector/semconv/v1.16.0" "google.golang.org/protobuf/proto" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator/header" ) @@ -39,8 +42,29 @@ const ( // Requirement Level: Optional // Examples: '228114450199004348' attributeDatadogSpanID = "datadog.span.id" + + tagRedisRawCommand = "redis.raw_command" + tagMemcachedCommand = "memcached.command" + tagMongoDBQuery = "mongodb.query" + tagElasticBody = "elasticsearch.body" + tagOpenSearchBody = "opensearch.body" + tagSQLQuery = "sql.query" + tagHTTPURL = "http.url" ) +type TracesTranslator struct { + obfuscator *obfuscate.Obfuscator + + conf *common.TracesConfig +} + +func NewTracesTranslator(config *common.Config) *TracesTranslator { + return &TracesTranslator{ + obfuscator: obfuscate.NewObfuscator(config.Traces.Obfuscation.Export()), + conf: &config.Traces, + } +} + func upsertHeadersAttributes(req *http.Request, attrs pcommon.Map) { if ddTracerVersion := req.Header.Get(header.TracerVersion); ddTracerVersion != "" { attrs.PutStr(semconv.AttributeTelemetrySDKVersion, "Datadog-"+ddTracerVersion) @@ -54,7 +78,7 @@ func upsertHeadersAttributes(req *http.Request, attrs pcommon.Map) { } } -func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces { +func (tt *TracesTranslator) ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces { var traces pb.Traces for _, p := range payload.GetChunks() { traces = append(traces, p.GetSpans()) @@ -91,6 +115,8 @@ func ToTraces(payload *pb.TracerPayload, req *http.Request) ptrace.Traces { for _, trace := range traces { for _, span := range trace { + tt.obfuscateSpan(span) + slice, exist := groupByService[span.Service] if !exist { slice = ptrace.NewSpanSlice() @@ -367,3 +393,86 @@ func uInt64ToSpanID(id uint64) pcommon.SpanID { binary.BigEndian.PutUint64(spanID[:], id) return spanID } + +func (tt *TracesTranslator) obfuscateSpan(span *pb.Span) { + if !tt.conf.Obfuscation.Enabled { + return + } + + o := tt.obfuscator + if tt.conf.Obfuscation.CreditCards.Enabled { + for k, v := range span.Meta { + newV := o.ObfuscateCreditCardNumber(k, v) + if v != newV { + span.Meta[k] = newV + } + } + } + + switch span.Type { + case "sql", "cassandra": + if span.Resource == "" { + return + } + oq, err := o.ObfuscateSQLString(span.Resource) + if err != nil { + // we have an error, discard the SQL to avoid polluting user resources. + span.Resource = textNonParsable + traceutil.SetMeta(span, tagSQLQuery, textNonParsable) + return + } + + span.Resource = oq.Query + if len(oq.Metadata.TablesCSV) > 0 { + traceutil.SetMeta(span, "sql.tables", oq.Metadata.TablesCSV) + } + traceutil.SetMeta(span, tagSQLQuery, oq.Query) + case "redis": + span.Resource = o.QuantizeRedisString(span.Resource) + if tt.conf.Obfuscation.Redis.Enabled { + if span.Meta == nil || span.Meta[tagRedisRawCommand] == "" { + return + } + if tt.conf.Obfuscation.Redis.RemoveAllArgs { + span.Meta[tagRedisRawCommand] = o.RemoveAllRedisArgs(span.Meta[tagRedisRawCommand]) + return + } + span.Meta[tagRedisRawCommand] = o.ObfuscateRedisString(span.Meta[tagRedisRawCommand]) + } + case "memcached": + if !tt.conf.Obfuscation.Memcached.Enabled { + return + } + if span.Meta == nil || span.Meta[tagMemcachedCommand] == "" { + return + } + span.Meta[tagMemcachedCommand] = o.ObfuscateMemcachedString(span.Meta[tagMemcachedCommand]) + case "web", "http": + if span.Meta == nil || span.Meta[tagHTTPURL] == "" { + return + } + span.Meta[tagHTTPURL] = o.ObfuscateURLString(span.Meta[tagHTTPURL]) + case "mongodb": + if !tt.conf.Obfuscation.Mongo.Enabled { + return + } + if span.Meta == nil || span.Meta[tagMongoDBQuery] == "" { + return + } + span.Meta[tagMongoDBQuery] = o.ObfuscateMongoDBString(span.Meta[tagMongoDBQuery]) + case "elasticsearch", "opensearch": + if span.Meta == nil { + return + } + if tt.conf.Obfuscation.ES.Enabled { + if span.Meta[tagElasticBody] != "" { + span.Meta[tagElasticBody] = o.ObfuscateElasticSearchString(span.Meta[tagElasticBody]) + } + } + if tt.conf.Obfuscation.OpenSearch.Enabled { + if span.Meta[tagOpenSearchBody] != "" { + span.Meta[tagOpenSearchBody] = o.ObfuscateOpenSearchString(span.Meta[tagOpenSearchBody]) + } + } + } +} diff --git a/receiver/datadogreceiver/internal/translator/traces_translator_test.go b/receiver/datadogreceiver/internal/translator/traces_translator_test.go index f7cdd3d3a1f3..96b9a99ed544 100644 --- a/receiver/datadogreceiver/internal/translator/traces_translator_test.go +++ b/receiver/datadogreceiver/internal/translator/traces_translator_test.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "log" "net/http" "strconv" "testing" @@ -19,6 +20,7 @@ import ( semconv "go.opentelemetry.io/collector/semconv/v1.16.0" "google.golang.org/protobuf/proto" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator/header" ) @@ -90,7 +92,9 @@ func TestTracePayloadV05Unmarshalling(t *testing.T) { tracePayloads, _ := HandleTracesPayload(req) assert.Len(t, tracePayloads, 1, "Expected one translated payload") tracePayload := tracePayloads[0] - translated := ToTraces(tracePayload, req) + + tt := NewTracesTranslator(common.NewConfig()) + translated := tt.ToTraces(tracePayload, req) assert.Equal(t, 1, translated.SpanCount(), "Span Count wrong") span := translated.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) assert.NotNil(t, span) @@ -110,6 +114,50 @@ func TestTracePayloadV05Unmarshalling(t *testing.T) { assert.Equal(t, 1.2, numericAttributeFloat) } +func TestTracePayloadV05UnmarshallingObfuscateSQL(t *testing.T) { + stringSlice, ok := data[0].([]string) + if !ok { + log.Fatal("Type assertion failed for data[0]") + } + + // Resource + stringSlice[7] = "/* query.digest=46f6d7bdae8dd8e8c907aed02b0e6525 tx=vjxn46eyxp */ \nselect /*+ MAX_EXECUTION_TIME(?) */ table.id as id1_6_0_, table.created_at as created_2_6_0_, table.updated_at as updated_3_6_0_, table.description as descript4_6_0_, table.is_internal as is_inter5_6_0_, table.name as name6_6_0_ \nfrom \ntable \nwhere table.id in (?, ?, ?, ?)" + + payload, err := vmsgp.Marshal(&data) + assert.NoError(t, err) + + var traces pb.Traces + require.NoError(t, traces.UnmarshalMsgDictionary(payload), "Must not error when marshaling content") + req, _ := http.NewRequest(http.MethodPost, "/v0.5/traces", io.NopCloser(bytes.NewReader(payload))) + + tracePayloads, _ := HandleTracesPayload(req) + assert.Len(t, tracePayloads, 1, "Expected one translated payload") + tracePayload := tracePayloads[0] + + cfg := common.NewConfig() + cfg.Traces.Obfuscation.Enabled = true + + tt := NewTracesTranslator(cfg) + translated := tt.ToTraces(tracePayload, req) + assert.Equal(t, 1, translated.SpanCount(), "Span Count wrong") + span := translated.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0) + assert.NotNil(t, span) + assert.Equal(t, "my-name", span.Name()) + assert.Equal(t, 11, span.Attributes().Len(), "missing attributes") + value, exists := span.Attributes().Get("service.name") + assert.True(t, exists, "service.name missing") + assert.Equal(t, "my-service", value.AsString(), "service.name attribute value incorrect") + serviceVersionValue, _ := span.Attributes().Get("service.version") + assert.Equal(t, "1.0.1", serviceVersionValue.AsString()) + spanResource, _ := span.Attributes().Get("dd.span.Resource") + assert.Equal(t, "select table.id, table.created_at, table.updated_at, table.description, table.is_internal, table.name from table where table.id in ( ? )", spanResource.Str()) + spanResource1, _ := span.Attributes().Get("sampling.priority") + assert.Equal(t, fmt.Sprintf("%f", 1.0), spanResource1.Str()) + numericAttributeValue, _ := span.Attributes().Get("numeric_attribute") + numericAttributeFloat, _ := strconv.ParseFloat(numericAttributeValue.AsString(), 64) + assert.Equal(t, 1.2, numericAttributeFloat) +} + func TestTracePayloadV07Unmarshalling(t *testing.T) { traces := getTraces(t) apiPayload := pb.TracerPayload{ diff --git a/receiver/datadogreceiver/receiver.go b/receiver/datadogreceiver/receiver.go index fb7cae448490..7b7585ecec2d 100644 --- a/receiver/datadogreceiver/receiver.go +++ b/receiver/datadogreceiver/receiver.go @@ -21,13 +21,14 @@ import ( "go.opentelemetry.io/collector/receiver/receiverhelper" "go.uber.org/zap" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/internal/translator/header" ) type datadogReceiver struct { address string - config *Config + config *common.Config params receiver.Settings nextTracesConsumer consumer.Traces @@ -35,6 +36,7 @@ type datadogReceiver struct { metricsTranslator *translator.MetricsTranslator statsTranslator *translator.StatsTranslator + tracesTranslator *translator.TracesTranslator server *http.Server tReceiver *receiverhelper.ObsReport @@ -132,7 +134,7 @@ func (ddr *datadogReceiver) getEndpoints() []Endpoint { return endpoints } -func newDataDogReceiver(config *Config, params receiver.Settings) (component.Component, error) { +func newDataDogReceiver(config *common.Config, params receiver.Settings) (component.Component, error) { instance, err := receiverhelper.NewObsReport(receiverhelper.ObsReportSettings{LongLivedCtx: false, ReceiverID: params.ID, Transport: "http", ReceiverCreateSettings: params}) if err != nil { return nil, err @@ -147,6 +149,7 @@ func newDataDogReceiver(config *Config, params receiver.Settings) (component.Com tReceiver: instance, metricsTranslator: translator.NewMetricsTranslator(params.BuildInfo), statsTranslator: translator.NewStatsTranslator(), + tracesTranslator: translator.NewTracesTranslator(config), }, nil } @@ -233,7 +236,7 @@ func (ddr *datadogReceiver) handleTraces(w http.ResponseWriter, req *http.Reques return } for _, ddTrace := range ddTraces { - otelTraces := translator.ToTraces(ddTrace, req) + otelTraces := ddr.tracesTranslator.ToTraces(ddTrace, req) spanCount = otelTraces.SpanCount() err = ddr.nextTracesConsumer.ConsumeTraces(obsCtx, otelTraces) if err != nil { diff --git a/receiver/datadogreceiver/receiver_test.go b/receiver/datadogreceiver/receiver_test.go index 52c1844b59fe..7cf8476c5d11 100644 --- a/receiver/datadogreceiver/receiver_test.go +++ b/receiver/datadogreceiver/receiver_test.go @@ -25,12 +25,14 @@ import ( "go.opentelemetry.io/collector/receiver/receivertest" "go.uber.org/multierr" "google.golang.org/protobuf/proto" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" ) func TestDatadogTracesReceiver_Lifecycle(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - cfg.(*Config).Endpoint = "localhost:0" + cfg.(*common.Config).Endpoint = "localhost:0" ddr, err := factory.CreateTraces(context.Background(), receivertest.NewNopSettings(), cfg, consumertest.NewNop()) assert.NoError(t, err, "Traces receiver should be created") @@ -44,7 +46,7 @@ func TestDatadogTracesReceiver_Lifecycle(t *testing.T) { func TestDatadogMetricsReceiver_Lifecycle(t *testing.T) { factory := NewFactory() cfg := factory.CreateDefaultConfig() - cfg.(*Config).Endpoint = "localhost:0" + cfg.(*common.Config).Endpoint = "localhost:0" ddr, err := factory.CreateMetrics(context.Background(), receivertest.NewNopSettings(), cfg, consumertest.NewNop()) assert.NoError(t, err, "Metrics receiver should be created") @@ -56,7 +58,7 @@ func TestDatadogMetricsReceiver_Lifecycle(t *testing.T) { } func TestDatadogServer(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address dd, err := newDataDogReceiver( cfg, @@ -213,7 +215,7 @@ func TestDatadogInfoEndpoint(t *testing.T) { } { tc := tc t.Run(tc.name, func(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address dd, err := newDataDogReceiver( @@ -252,7 +254,7 @@ func TestDatadogInfoEndpoint(t *testing.T) { } func TestDatadogMetricsV1_EndToEnd(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address sink := new(consumertest.MetricsSink) @@ -314,7 +316,7 @@ func TestDatadogMetricsV1_EndToEnd(t *testing.T) { } func TestDatadogMetricsV2_EndToEnd(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address sink := new(consumertest.MetricsSink) @@ -394,7 +396,7 @@ func TestDatadogMetricsV2_EndToEnd(t *testing.T) { } func TestDatadogSketches_EndToEnd(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address sink := new(consumertest.MetricsSink) @@ -481,7 +483,7 @@ func TestDatadogSketches_EndToEnd(t *testing.T) { } func TestStats_EndToEnd(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address sink := new(consumertest.MetricsSink) @@ -572,7 +574,7 @@ func TestStats_EndToEnd(t *testing.T) { } func TestDatadogServices_EndToEnd(t *testing.T) { - cfg := createDefaultConfig().(*Config) + cfg := createDefaultConfig().(*common.Config) cfg.Endpoint = "localhost:0" // Using a randomly assigned address sink := new(consumertest.MetricsSink) diff --git a/testbed/datareceivers/datadog.go b/testbed/datareceivers/datadog.go index 6bba0484efc0..7ed128bc5b66 100644 --- a/testbed/datareceivers/datadog.go +++ b/testbed/datareceivers/datadog.go @@ -11,6 +11,7 @@ import ( "go.opentelemetry.io/collector/receiver" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver" + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/datadogreceiver/common" "github.com/open-telemetry/opentelemetry-collector-contrib/testbed/testbed" ) @@ -28,7 +29,7 @@ func NewDataDogDataReceiver() testbed.DataReceiver { func (dd *datadogDataReceiver) Start(tc consumer.Traces, _ consumer.Metrics, _ consumer.Logs) error { factory := datadogreceiver.NewFactory() - cfg := factory.CreateDefaultConfig().(*datadogreceiver.Config) + cfg := factory.CreateDefaultConfig().(*common.Config) cfg.Endpoint = "0.0.0.0:8126" set := receiver.Settings{}