diff --git a/manifests/cpp.yml b/manifests/cpp.yml index 8185d0cc4d..2011289291 100644 --- a/manifests/cpp.yml +++ b/manifests/cpp.yml @@ -145,7 +145,10 @@ tests/: test_inferred_proxy.py: Test_AWS_API_Gateway_Inferred_Span_Creation: missing_feature test_otel_drop_in.py: - Test_Otel_Drop_In: missing_feature + Test_Otel_Drop_In: irrelevant (library does not implement OpenTelemetry) + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: irrelevant (library does not implement OpenTelemetry) parametric/: test_128_bit_traceids.py: Test_128_Bit_Traceids: v1.0.1.dev diff --git a/manifests/dotnet.yml b/manifests/dotnet.yml index 0f4825c6f3..99a1705f81 100644 --- a/manifests/dotnet.yml +++ b/manifests/dotnet.yml @@ -409,6 +409,9 @@ tests/: Test_AWS_API_Gateway_Inferred_Span_Creation: missing_feature test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: v3.9.0 parametric/: test_config_consistency.py: Test_Config_Dogstatsd: missing_feature (does not support hostname) diff --git a/manifests/golang.yml b/manifests/golang.yml index 1e118d62a7..6c9c36bb1a 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -539,6 +539,11 @@ tests/: Test_AWS_API_Gateway_Inferred_Span_Creation: missing_feature test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: + '*': incomplete_test_app (endpoint not implemented) + net-http: v1.70.1 parametric/: test_config_consistency.py: Test_Config_Dogstatsd: v1.72.0-dev diff --git a/manifests/java.yml b/manifests/java.yml index 516ad33e20..cabebd7455 100644 --- a/manifests/java.yml +++ b/manifests/java.yml @@ -1597,6 +1597,11 @@ tests/: spring-boot: v1.39.0 test_sql.py: Test_Sql: bug (APMAPI-729) + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: + '*': incomplete_test_app (endpoint not implemented) + spring-boot: v1.39.0 parametric/: test_config_consistency.py: Test_Config_Dogstatsd: missing_feature (default hostname is inconsistent) diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 322fc67948..a72ff37fcc 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -787,6 +787,12 @@ tests/: express5: *ref_5_26_0 test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: + '*': incomplete_test_app (endpoint not implemented) + express4: *ref_5_26_0 + express5: *ref_5_26_0 parametric/: test_128_bit_traceids.py: Test_128_Bit_Traceids: *ref_3_0_0 diff --git a/manifests/php.yml b/manifests/php.yml index c270c562e9..2900dd0c2a 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -382,6 +382,9 @@ tests/: Test_AWS_API_Gateway_Inferred_Span_Creation: missing_feature test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: incomplete_test_app (endpoint not implemented) parametric/: test_128_bit_traceids.py: Test_128_Bit_Traceids: v0.84.0 diff --git a/manifests/python.yml b/manifests/python.yml index c0a0bdfb5c..28ce197858 100644 --- a/manifests/python.yml +++ b/manifests/python.yml @@ -761,6 +761,11 @@ tests/: Test_AWS_API_Gateway_Inferred_Span_Creation: missing_feature test_otel_drop_in.py: Test_Otel_Drop_In: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: + '*': incomplete_test_app (endpoint not implemented) + flask-poc: v2.19.0 parametric/: test_128_bit_traceids.py: Test_128_Bit_Traceids: v2.6.0 diff --git a/manifests/ruby.yml b/manifests/ruby.yml index 2226134854..878b275c1e 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -405,6 +405,11 @@ tests/: TestK8sLibInjectioProfilingClusterEnabled: missing_feature TestK8sLibInjectioProfilingClusterOverride: missing_feature TestK8sLibInjectioProfilingDisabledByDefault: missing_feature + otel/: + test_context_propagation.py: + Test_Otel_Context_Propagation_Default_Propagator_Api: + '*': incomplete_test_app (endpoint not implemented) + rails70: v2.0.0 parametric/: test_config_consistency.py: Test_Config_Dogstatsd: missing_feature diff --git a/tests/otel/test_context_propagation.py b/tests/otel/test_context_propagation.py new file mode 100644 index 0000000000..32d579e171 --- /dev/null +++ b/tests/otel/test_context_propagation.py @@ -0,0 +1,41 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the the Apache License Version 2.0. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2024 Datadog, Inc. + +import json +from utils import weblog, interfaces, scenarios, features, incomplete_test_app + + +@features.otel_propagators_api +@scenarios.apm_tracing_e2e_otel +class Test_Otel_Context_Propagation_Default_Propagator_Api: + def setup_propagation_extract(self): + extract_headers = { + "traceparent": "00-11111111111111110000000000000002-000000000000000a-01", + "tracestate": "dd=s:2;p:000000000000000a,foo=1", + "baggage": "foo=1", + } + self.r = weblog.get("/otel_drop_in_default_propagator_extract", headers=extract_headers) + + @incomplete_test_app(library="nodejs", reason="Node.js extract endpoint doesn't seem to be working.") + @incomplete_test_app(library="ruby", reason="Ruby extract seems to fail even though it should be supported") + def test_propagation_extract(self): + content = json.loads(self.r.text) + + assert content["trace_id"] == 2 + assert content["span_id"] == 10 + assert content["tracestate"] and not content["tracestate"].isspace() + # assert content["baggage"] and not content["baggage"].isspace() + + def setup_propagation_inject(self): + inject_headers = { + "baggage": "foo=2", + } + self.r = weblog.get("/otel_drop_in_default_propagator_inject") + + @incomplete_test_app(library="nodejs", reason="Node.js inject endpoint doesn't seem to be working.") + def test_propagation_inject(self): + content = json.loads(self.r.text) + + assert content["traceparent"] and not content["traceparent"].isspace() + # assert content["baggage"] and not content["baggage"].isspace() diff --git a/utils/_features.py b/utils/_features.py index c165472189..b525f162d0 100644 --- a/utils/_features.py +++ b/utils/_features.py @@ -2372,5 +2372,14 @@ def debugger_code_origins(test_object): pytest.mark.features(feature_id=360)(test_object) return test_object + @staticmethod + def otel_propagators_api(test_object): + """OpenTelemetry Propagators API + + https://feature-parity.us1.prod.dog/#/?feature=361 + """ + pytest.mark.features(feature_id=361)(test_object) + return test_object + features = _Features() diff --git a/utils/build/docker/dotnet/weblog/Endpoints/OtelDropInEndpoint.cs b/utils/build/docker/dotnet/weblog/Endpoints/OtelDropInEndpoint.cs new file mode 100644 index 0000000000..a26b950ec0 --- /dev/null +++ b/utils/build/docker/dotnet/weblog/Endpoints/OtelDropInEndpoint.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; + +namespace weblog +{ + public class OtelDropInEndpoint : ISystemTestEndpoint + { + public void Register(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routeBuilder) + { + routeBuilder.MapGet("/otel_drop_in_default_propagator_extract", async context => + { + var parentContext = OpenTelemetryInstrumentation.Propagator.Extract(default, context.Request.Headers, (carrier, key) => + { + return carrier.TryGetValue(key, out var value) && value.Count >= 1 ? new[] { value[0] } : null; + }); + + var ddTraceId = Convert.ToUInt64(parentContext.ActivityContext.TraceId.ToHexString().Substring(16), 16); + var ddSpanId = Convert.ToUInt64(parentContext.ActivityContext.SpanId.ToHexString(), 16); + + var data = new + { + trace_id = ddTraceId, + span_id = ddSpanId, + tracestate = parentContext.ActivityContext.TraceState, + baggage = parentContext.Baggage + }; + + await context.Response.WriteAsync(JsonSerializer.Serialize(data)); + }); + + routeBuilder.MapGet("/otel_drop_in_default_propagator_inject", async context => + { + var headersDict = new Dictionary(); + OpenTelemetryInstrumentation.Propagator.Inject(new PropagationContext(Activity.Current.Context, Baggage.Current), headersDict, (carrier, key, value) => + { + carrier[key] = value; + }); + + await context.Response.WriteAsync(JsonSerializer.Serialize(headersDict)); + }); + } + } +} diff --git a/utils/build/docker/dotnet/weblog/OpenTelemetryInstrumentation.cs b/utils/build/docker/dotnet/weblog/OpenTelemetryInstrumentation.cs new file mode 100644 index 0000000000..a2797162b5 --- /dev/null +++ b/utils/build/docker/dotnet/weblog/OpenTelemetryInstrumentation.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; +using OpenTelemetry.Context.Propagation; + +namespace weblog +{ + public static class OpenTelemetryInstrumentation + { + public static TextMapPropagator Propagator { get; } = Propagators.DefaultTextMapPropagator; + } +} diff --git a/utils/build/docker/dotnet/weblog/app.csproj b/utils/build/docker/dotnet/weblog/app.csproj index b17f904def..1e5f4cd0c7 100644 --- a/utils/build/docker/dotnet/weblog/app.csproj +++ b/utils/build/docker/dotnet/weblog/app.csproj @@ -44,5 +44,6 @@ + diff --git a/utils/build/docker/golang/app/net-http/main.go b/utils/build/docker/golang/app/net-http/main.go index ebbfd3dff2..497d05f01e 100644 --- a/utils/build/docker/golang/app/net-http/main.go +++ b/utils/build/docker/golang/app/net-http/main.go @@ -32,6 +32,7 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" oteltrace "go.opentelemetry.io/otel/trace" + otelbaggage "go.opentelemetry.io/otel/baggage" "gopkg.in/DataDog/dd-trace-go.v1/appsec" httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" @@ -537,6 +538,79 @@ func main() { w.Write([]byte("ok")) }) + mux.HandleFunc("/otel_drop_in_default_propagator_extract", func(w http.ResponseWriter, r *http.Request) { + // Differing from other languages, the user must set the text map propagator because dd-trace-go + // doesn't automatically instrument at runtime (not including Orchestrion) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + httpCarrier := HttpCarrier{header: r.Header} + + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), httpCarrier) + + spanContext := oteltrace.SpanContextFromContext(ctx) + baggage := otelbaggage.FromContext(ctx) + + base := 16 + bitSize := 64 + result := make(map[string]any, 4) + + num, err := strconv.ParseInt(spanContext.TraceID().String()[16:], base, bitSize) + if err == nil { + result["trace_id"] = num + } + + num, err = strconv.ParseInt(spanContext.SpanID().String(), base, bitSize) + if err == nil { + result["span_id"] = num + } + + result["tracestate"] = spanContext.TraceState().String() + result["baggage"] = baggage.String() + + jsonData, err := json.Marshal(result) + if err != nil { + w.WriteHeader(422) + w.Write([]byte("failed to convert carrier to JSON")) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(jsonData) + }) + + mux.HandleFunc("/otel_drop_in_default_propagator_inject", func(w http.ResponseWriter, r *http.Request) { + // Differing from other languages, the user must set the text map propagator because dd-trace-go + // doesn't automatically instrument at runtime (not including Orchestrion) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + ctx := context.Background() + p := ddotel.NewTracerProvider() + tracer := p.Tracer("") + otel.SetTracerProvider(p) + + _, span := tracer.Start(ddotel.ContextWithStartOptions(ctx), "main") + newCtx := oteltrace.ContextWithSpan(ctx, span) + + propagator := otel.GetTextMapPropagator() + mapCarrier := make(MapCarrier) + propagator.Inject(newCtx, mapCarrier) + + jsonData, err := json.Marshal(mapCarrier) + span.End() + + if err != nil { + w.WriteHeader(422) + w.Write([]byte("failed to convert carrier to JSON")) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(jsonData) + }) + mux.HandleFunc("/session/new", func(w http.ResponseWriter, r *http.Request) { sessionID := strconv.Itoa(rand.Int()) w.Header().Add("Set-Cookie", "session="+sessionID+"; Path=/; Max-Age=3600; Secure; HttpOnly") @@ -598,6 +672,44 @@ func (c carrier) ForeachKey(handler func(key, val string) error) error { return nil } +type MapCarrier map[string]string + +func (c MapCarrier) Get(key string) string { + return c[key] +} + +func (c MapCarrier) Set(key, val string) { + c[key] = val +} + +func (c MapCarrier) Keys() []string { + keys := make([]string, 0, len(c)) + for k := range c { + keys = append(keys, k) + } + return keys +} + +type HttpCarrier struct { + header http.Header +} + +func (c HttpCarrier) Get(key string) string { + return c.header.Get(key) +} + +func (c HttpCarrier) Set(key, val string) { + c.header.Set(key, val) +} + +func (c HttpCarrier) Keys() []string { + keys := make([]string, 0, len(c.header)) + for k := range c.header { + keys = append(keys, k) + } + return keys +} + func write(w http.ResponseWriter, r *http.Request, d []byte) { span, _ := ddtracer.StartSpanFromContext(r.Context(), "child.span") defer span.Finish() diff --git a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/App.java b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/App.java index 0767b37c0d..4bbabf87ff 100644 --- a/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/App.java +++ b/utils/build/docker/java/spring-boot/src/main/java/com/datadoghq/system_tests/springboot/App.java @@ -32,13 +32,19 @@ import org.springframework.web.server.ResponseStatusException; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; import io.opentelemetry.context.Scope; +import io.opentelemetry.context.Context; import io.opentracing.Span; import io.opentracing.util.GlobalTracer; import ognl.Ognl; @@ -1094,6 +1100,60 @@ public String otelDropInSpan() { return "OK"; } + @RequestMapping("/otel_drop_in_default_propagator_extract") + public String otelDropInDefaultPropagatorExtract(@RequestHeader Map headers) throws com.fasterxml.jackson.core.JsonProcessingException { + ContextPropagators propagators = GlobalOpenTelemetry.getPropagators(); + TextMapPropagator textMapPropagator = propagators.getTextMapPropagator(); + + Context extractedContext = textMapPropagator.extract(Context.current(), headers, new TextMapGetter>() { + @Override + public Iterable keys(Map map) { + return map.keySet(); + } + + @Override + public String get(Map map, String key) { + return map.get(key); + } + }); + + io.opentelemetry.api.trace.SpanContext spanContext = io.opentelemetry.api.trace.Span.fromContext(extractedContext).getSpanContext(); + Long ddTraceId = Long.parseLong(spanContext.getTraceId().substring(16), 16); + Long ddSpanId = Long.parseLong(spanContext.getSpanId(), 16); + + Map map = new HashMap<>(); + map.put("trace_id", ddTraceId); + map.put("span_id", ddSpanId); + map.put("tracestate", spanContext.getTraceState().asMap().toString()); + map.put("baggage", Baggage.fromContext(extractedContext).asMap().toString()); + + // Convert headers map to JSON string + ObjectMapper mapper = new ObjectMapper(); + String jsonString = mapper.writeValueAsString(map); + + return jsonString; + } + + @RequestMapping("/otel_drop_in_default_propagator_inject") + public String otelDropInDefaultPropagatorInject() throws com.fasterxml.jackson.core.JsonProcessingException { + ContextPropagators propagators = GlobalOpenTelemetry.getPropagators(); + TextMapPropagator textMapPropagator = propagators.getTextMapPropagator(); + + Map map = new HashMap<>(); + textMapPropagator.inject(Context.current(), map, new TextMapSetter>() { + @Override + public void set(Map map, String key, String value) { + map.put(key, value); + } + }); + + // Convert headers map to JSON string + ObjectMapper mapper = new ObjectMapper(); + String jsonString = mapper.writeValueAsString(map); + + return jsonString; + } + @GetMapping(value = "/requestdownstream") public String requestdownstream(HttpServletResponse response) throws IOException { String url = "http://localhost:7777/returnheaders"; diff --git a/utils/build/docker/nodejs/express/app.js b/utils/build/docker/nodejs/express/app.js index 2ac3da2728..1e573030df 100644 --- a/utils/build/docker/nodejs/express/app.js +++ b/utils/build/docker/nodejs/express/app.js @@ -12,6 +12,7 @@ const fs = require('fs') const passport = require('passport') const crypto = require('crypto') const pino = require('pino') +const api = require('@opentelemetry/api') const iast = require('./iast') const dsm = require('./dsm') @@ -429,6 +430,29 @@ app.get('/db', async (req, res) => { } }) +app.get('/otel_drop_in_default_propagator_extract', (req, res) => { + const ctx = api.propagation.extract(api.context.active(), req.headers) + const spanContext = api.trace.getSpan(ctx).spanContext() + + const result = {} + result.trace_id = parseInt(spanContext.traceId.substring(16), 16) + result.span_id = parseInt(spanContext.spanId, 16) + result.tracestate = spanContext.traceState.serialize() + // result.baggage = api.propagation.getBaggage(spanContext).toString() + + res.json(result) +}) + +app.get('/otel_drop_in_default_propagator_inject', (req, res) => { + const tracer = api.trace.getTracer('my-application', '0.1.0') + const span = tracer.startSpan('main') + const result = {} + + api.propagation.inject( + api.trace.setSpanContext(api.ROOT_CONTEXT, span.spanContext()), result, api.defaultTextMapSetter) + res.json(result) +}) + app.post('/shell_execution', (req, res) => { const options = { shell: !!req?.body?.options?.shell } const reqArgs = req?.body?.args diff --git a/utils/build/docker/nodejs/express/package-lock.json b/utils/build/docker/nodejs/express/package-lock.json index 32cf5b06e4..30bb493c07 100644 --- a/utils/build/docker/nodejs/express/package-lock.json +++ b/utils/build/docker/nodejs/express/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1", "amqplib": "^0.10.3", "aws-sdk": "^2.1530.0", "axios": "1.2.3", @@ -544,6 +546,36 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -5758,6 +5790,26 @@ "fastq": "^1.6.0" } }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" + }, + "@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "requires": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "dependencies": { + "@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==" + } + } + }, "@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", diff --git a/utils/build/docker/nodejs/express/package.json b/utils/build/docker/nodejs/express/package.json index df490866ae..a20e37761b 100644 --- a/utils/build/docker/nodejs/express/package.json +++ b/utils/build/docker/nodejs/express/package.json @@ -10,6 +10,8 @@ "author": "", "license": "ISC", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1", "amqplib": "^0.10.3", "aws-sdk": "^2.1530.0", "axios": "1.2.3", diff --git a/utils/build/docker/python/flask/app.py b/utils/build/docker/python/flask/app.py index 16fdcd823e..de5ca1a201 100644 --- a/utils/build/docker/python/flask/app.py +++ b/utils/build/docker/python/flask/app.py @@ -40,6 +40,10 @@ from iast import weak_hash_multiple from iast import weak_hash_secure_algorithm import requests +import opentelemetry.baggage +import opentelemetry.context +import opentelemetry.propagate +import opentelemetry.trace if os.environ.get("INCLUDE_SQLSERVER", "true") == "true": @@ -1499,3 +1503,29 @@ def s3_multipart_upload(): result = {"result": "ok", "object": {"e_tag": response.e_tag.replace('"', "")}} return jsonify(result) + + +@app.route("/otel_drop_in_default_propagator_extract", methods=["GET"]) +def otel_drop_in_default_propagator_extract(): + def get_header_from_flask_request(request, key): + return request.headers.get(key) + + context = opentelemetry.propagate.extract(flask_request.headers, opentelemetry.context.get_current()) + + span_context = opentelemetry.trace.get_current_span(context).get_span_context() + + result = {} + result["trace_id"] = int(format(span_context.trace_id, "032x")[16:], 16) + result["span_id"] = span_context.span_id + result["tracestate"] = str(span_context.trace_state) + result["baggage"] = str(opentelemetry.baggage.get_all(context)) + + return jsonify(result) + + +@app.route("/otel_drop_in_default_propagator_inject", methods=["GET"]) +def otel_drop_in_default_propagator_inject(): + result = {} + opentelemetry.propagate.inject(result, opentelemetry.context.get_current()) + + return jsonify(result) diff --git a/utils/build/docker/ruby/rails70/Gemfile b/utils/build/docker/ruby/rails70/Gemfile index a17cc39ecd..a9f8054686 100644 --- a/utils/build/docker/ruby/rails70/Gemfile +++ b/utils/build/docker/ruby/rails70/Gemfile @@ -30,6 +30,10 @@ gem "jbuilder" # Talk with Kafka for propagation tests gem "ruby-kafka" +# Use the OpenTelemetry API and SDK +gem "opentelemetry-api" +gem "opentelemetry-sdk" + # Use Redis adapter to run Action Cable in production # gem "redis", "~> 4.0" diff --git a/utils/build/docker/ruby/rails70/Gemfile.lock b/utils/build/docker/ruby/rails70/Gemfile.lock index b89e5065f2..9f55d48399 100644 --- a/utils/build/docker/ruby/rails70/Gemfile.lock +++ b/utils/build/docker/ruby/rails70/Gemfile.lock @@ -168,6 +168,18 @@ GEM racc (~> 1.4) nokogiri (1.13.1-x86_64-linux) racc (~> 1.4) + opentelemetry-api (1.4.0) + opentelemetry-common (0.21.0) + opentelemetry-api (~> 1.0) + opentelemetry-registry (0.3.1) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.6.0) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.10.1) + opentelemetry-api (~> 1.0) orm_adapter (0.5.0) pry (0.14.1) coderay (~> 1.1) @@ -257,11 +269,11 @@ GEM zeitwerk (2.5.4) PLATFORMS - aarch64-linux-gnu + aarch64-linux arm64-darwin ruby x86_64-darwin - x86_64-linux-gnu + x86_64-linux DEPENDENCIES bootsnap @@ -271,6 +283,8 @@ DEPENDENCIES devise importmap-rails jbuilder + opentelemetry-api + opentelemetry-sdk pry puma (~> 5.0) rails (~> 7.0.1) diff --git a/utils/build/docker/ruby/rails70/app/controllers/system_test_controller.rb b/utils/build/docker/ruby/rails70/app/controllers/system_test_controller.rb index 53ec7001e5..9d8fd1bb76 100644 --- a/utils/build/docker/ruby/rails70/app/controllers/system_test_controller.rb +++ b/utils/build/docker/ruby/rails70/app/controllers/system_test_controller.rb @@ -2,6 +2,7 @@ require 'datadog/kit/appsec/events' require 'kafka' +require 'opentelemetry' class SystemTestController < ApplicationController skip_before_action :verify_authenticity_token @@ -250,4 +251,34 @@ def return_headers end render json: JSON.generate(request_headers), content_type: 'application/json' end + + def otel_drop_in_default_propagator_extract + # The extract operation succeeds with a custom OpenTelemetry propagator, but not with the default one. + # To see this, uncomment the next line, and use that propagator to do the context extraction + # propagator = OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators([OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator, OpenTelemetry::Baggage::Propagation.text_map_propagator]) + context = OpenTelemetry.propagation.extract(request.headers) + + span_context = OpenTelemetry::Trace.current_span(context).context + + baggage = OpenTelemetry::Baggage.raw_entries() + baggage_str = "" + baggage.each_pair do |key, value| + baggage_str << value << ',' + end + baggage_str.chop! + + result = {} + result["trace_id"] = span_context.hex_trace_id.from(16).to_i(16) + result["span_id"] = span_context.hex_span_id.to_i(16) + result["tracestate"] = span_context.tracestate.to_s + result["baggage"] = baggage_str + + render json: JSON.generate(result), content_type: 'application/json' + end + + def otel_drop_in_default_propagator_inject + headers = {} + OpenTelemetry.propagation.inject(headers) + render json: JSON.generate(headers), content_type: 'application/json' + end end diff --git a/utils/build/docker/ruby/rails70/config/initializers/datadog.rb b/utils/build/docker/ruby/rails70/config/initializers/datadog.rb index e7079ecc33..08ecc3be10 100644 --- a/utils/build/docker/ruby/rails70/config/initializers/datadog.rb +++ b/utils/build/docker/ruby/rails70/config/initializers/datadog.rb @@ -1,7 +1,13 @@ +require 'opentelemetry/sdk' +require 'datadog/opentelemetry' + Datadog.configure do |c| c.diagnostics.debug = true end +::OpenTelemetry::SDK.configure do |_c| +end + # Send non-web init event if defined?(Datadog::Tracing) diff --git a/utils/build/docker/ruby/rails70/config/routes.rb b/utils/build/docker/ruby/rails70/config/routes.rb index 5624e4b3e1..46da54cdfb 100644 --- a/utils/build/docker/ruby/rails70/config/routes.rb +++ b/utils/build/docker/ruby/rails70/config/routes.rb @@ -48,6 +48,9 @@ get '/requestdownstream' => 'system_test#request_downstream' get '/returnheaders' => 'system_test#return_headers' + get '/otel_drop_in_default_propagator_extract' => 'system_test#otel_drop_in_default_propagator_extract' + get '/otel_drop_in_default_propagator_inject' => 'system_test#otel_drop_in_default_propagator_inject' + get '/debugger/init' => 'debugger#init' get '/debugger/pii' => 'debugger#pii' get '/debugger/log' => 'debugger#log_probe'