Skip to content

Commit b86a291

Browse files
authored
OTEL: Update Operation Name to Match Frontend Usage (#1033)
## Overview OTEL Lambda spans emitted by the `opentelemetry.instrumentation.aws_lambda` Python instrumentation come through the Extension with `name: server.request` — the OTEL semantic-convention generic-server fallback. The Datadog Serverless page invocation table filters by: ``` operation_name:("aws.lambda" OR "aws.lambda.function" OR "opentelemetry_instrumentation_aws_lambda.server" OR "opentelemetry.instrumentation.aws_lambda.server") ``` `server.request` matches none of those, so OTEL Lambda functions show 0 traces on the Serverless page even though the spans land in APM correctly. This PR overrides the operation name to `aws.lambda` for OTEL Lambda root-invocation spans on the Extension side. Detection is gated on the instrumentation scope being `opentelemetry.instrumentation.aws_lambda` AND span kind being `Server`, so other spans in the trace (HTTP clients, internal spans, custom spans) are not touched. ## What Changed `bottlecap/src/otlp/transform.rs` — `get_otel_operation_name_v2`: - Threads the OTEL `InstrumentationScope` reference through to the operation-name resolver - Adds an early branch: `lib.name == "opentelemetry.instrumentation.aws_lambda"` AND `span.kind == Server` → `"aws.lambda"` - All other spans continue to resolve via the existing HTTP / DB / messaging / RPC / FAAS / generic fallback chain - The `lib` parameter was already available at the call site in `otel_span_to_dd_span`; the change just plumbs it into `get_otel_operation_name_v2` ## Why on the Extension (vs. logs-backend) The complementary path — spans arriving through the OTLP endpoint without going through the Extension — is handled by DataDog/logs-backend#134974, which performs the same `server.request → aws.lambda` override in `SpansOTLPProtobufPayloadParser` along with the broader OTEL → Datadog field remap (`cloud.resource_id → function_arn`, `faas.invocation_id → request_id`, etc.). Doing the override on the Extension side for the Extension-forwarded path is preferred because: 1. The override is closer to the source of truth — the Extension is the only component that knows the span came from the OTEL `aws_lambda` instrumentation 2. APM stats / error tracking / rule-based sampling all see the same `aws.lambda` name, avoiding "named differently in different places" bugs 3. Mirrors how the Extension already overrides other OTEL-side fields with Datadog conventions ## Testing Existing `test_otel_operation_name_*` extended to assert `server.request` is still produced when the lib name does **not** match the AWS Lambda scope (regression coverage for the threading change). New `test_otel_operation_name_aws_lambda` covers three cases: 1. AWS Lambda scope + `Server` kind → `aws.lambda` 2. AWS Lambda scope + `Internal` kind (handler child spans) → unchanged (`SPAN_KIND_INTERNAL`) 3. Different scope (e.g., `handler`) + `Server` kind → unchanged (`server.request`) ## Companion PRs - **logs-backend** [#134974](DataDog/logs-backend#134974) — same operation-name override for the OTLP-endpoint path, plus the OTEL → Datadog field remap - **web-ui** [#300855](DataDog/web-ui#300855) — defensive `request_id` fallback in the trace list hook + widened log queries that match OTLP-ingested logs alongside Datadog Forwarder logs Together with this PR, OTEL-instrumented AWS Lambda functions (both endpoint-only and Extension-forwarded) work end-to-end on the Datadog Serverless page: trace discovery, invocation correlation, function-level log discovery, error filter, log count.
1 parent 47d70b3 commit b86a291

1 file changed

Lines changed: 66 additions & 3 deletions

File tree

bottlecap/src/otlp/transform.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ pub const KEY_DATADOG_STATS_COMPUTED: &str = "_dd.stats_computed";
5858

5959
// const SPAN_TYPE_GENERIC_DB: &str = "db";
6060

61+
// AWS Lambda OTEL instrumentation scope name
62+
const AWS_LAMBDA_INSTRUMENTATION_SCOPE: &str = "opentelemetry.instrumentation.aws_lambda";
63+
6164
// TODO: add mappings
6265
#[allow(dead_code)]
6366
static DB_SYSTEM_MAP: LazyLock<HashMap<String, String>> = LazyLock::new(HashMap::new);
@@ -251,7 +254,7 @@ fn get_otel_operation_name_v1(
251254
}
252255

253256
#[allow(clippy::too_many_lines)]
254-
fn get_otel_operation_name_v2(otel_span: &OtelSpan) -> String {
257+
fn get_otel_operation_name_v2(otel_span: &OtelSpan, lib: &OtelInstrumentationScope) -> String {
255258
let operation_name =
256259
get_otel_attribute_value_as_string(&otel_span.attributes, "operation.name", false);
257260
if !operation_name.is_empty() {
@@ -261,6 +264,17 @@ fn get_otel_operation_name_v2(otel_span: &OtelSpan) -> String {
261264
let is_client = otel_span.kind() == SpanKind::Client;
262265
let is_server = otel_span.kind() == SpanKind::Server;
263266

267+
// AWS Lambda: rename the OTEL aws-lambda instrumentation's invocation span (always
268+
// emitted with SpanKind::Server) to "aws.lambda" so it matches the Datadog Serverless
269+
// page's LAMBDA_INVOCATION_SPAN_FILTER. The span may have a remote parent when the
270+
// caller propagates trace context (e.g. API Gateway → Lambda) — that is still the
271+
// Lambda invocation span we want to rename. Other spans the same instrumentation
272+
// emits (e.g. SDK client spans inside the handler) are not Server kind and pass
273+
// through to the existing fallback chain unchanged.
274+
if is_server && lib.name == AWS_LAMBDA_INSTRUMENTATION_SCOPE {
275+
return "aws.lambda".to_string();
276+
}
277+
264278
// HTTP
265279
let method =
266280
get_otel_attribute_value_as_string(&otel_span.attributes, "http.request.method", false);
@@ -894,7 +908,7 @@ pub fn otel_span_to_dd_span(
894908
}
895909

896910
if otel_operation_and_resource_v2_enabled(config.clone()) {
897-
dd_span.name = get_otel_operation_name_v2(otel_span);
911+
dd_span.name = get_otel_operation_name_v2(otel_span, lib);
898912
} else {
899913
dd_span.name = get_otel_operation_name_v1(
900914
otel_span,
@@ -1325,6 +1339,55 @@ mod tests {
13251339
get_otel_operation_name_v1(&otel_span, &lib, false, &HashMap::new(), true),
13261340
"opentelemetry_instrumentation_aws_lambda.server"
13271341
);
1328-
assert_eq!(get_otel_operation_name_v2(&otel_span), "server.request");
1342+
// With a non-matching lib name, should return server.request
1343+
assert_eq!(
1344+
get_otel_operation_name_v2(&otel_span, &lib),
1345+
"server.request"
1346+
);
1347+
}
1348+
1349+
#[test]
1350+
fn test_otel_operation_name_aws_lambda() {
1351+
// Test that AWS Lambda OTEL instrumentation gets aws.lambda operation name
1352+
let otel_span = OtelSpan {
1353+
name: "handler.handler".to_string(),
1354+
kind: SpanKind::Server as i32,
1355+
..Default::default()
1356+
};
1357+
let aws_lambda_lib = OtelInstrumentationScope {
1358+
name: "opentelemetry.instrumentation.aws_lambda".to_string(),
1359+
version: "0.42b0".to_string(),
1360+
attributes: [].to_vec(),
1361+
dropped_attributes_count: 0,
1362+
};
1363+
1364+
// AWS Lambda Server span should return aws.lambda
1365+
assert_eq!(
1366+
get_otel_operation_name_v2(&otel_span, &aws_lambda_lib),
1367+
"aws.lambda"
1368+
);
1369+
1370+
// Non-server span from AWS Lambda instrumentation should NOT return aws.lambda
1371+
let internal_span = OtelSpan {
1372+
name: "my-function".to_string(),
1373+
kind: SpanKind::Internal as i32,
1374+
..Default::default()
1375+
};
1376+
assert_eq!(
1377+
get_otel_operation_name_v2(&internal_span, &aws_lambda_lib),
1378+
"SPAN_KIND_INTERNAL"
1379+
);
1380+
1381+
// Server span from different instrumentation should return server.request
1382+
let other_lib = OtelInstrumentationScope {
1383+
name: "handler".to_string(),
1384+
version: String::new(),
1385+
attributes: [].to_vec(),
1386+
dropped_attributes_count: 0,
1387+
};
1388+
assert_eq!(
1389+
get_otel_operation_name_v2(&otel_span, &other_lib),
1390+
"server.request"
1391+
);
13291392
}
13301393
}

0 commit comments

Comments
 (0)