Skip to content

Commit bf9a59b

Browse files
feat: create otel metrics for lambda (#39)
1 parent d3e5a78 commit bf9a59b

17 files changed

Lines changed: 675 additions & 33 deletions

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,82 @@ The following environment variables allow fine-grained control over secret maski
9898
Example: `DASH0_MASK_QUERY_PARAMS='[".*api_key.*", ".*token.*"]'`
9999

100100

101+
## Enrichment Attributes
102+
103+
The extension enriches telemetry data with additional attributes beyond what the auto-instrumentation provides.
104+
105+
### Span Attributes
106+
107+
The following attributes are added to spans by the extension (if relevant):
108+
109+
| Attribute | Type | Description |
110+
|---|---|---|
111+
| `faas.invocation_id` | string | The AWS request ID of the current invocation. |
112+
| `faas.trigger` | string | The event source that triggered the Lambda (e.g., `aws:sqs`, `aws:dynamodb`, `aws:event_bridge`). Extracted from the event payload. |
113+
| `faas.init_duration` | double | The cold start initialization duration in milliseconds. Only present on cold start invocations. |
114+
| `dash0.faas.record_count` | int | The number of records in a batch event (SQS, DynamoDB Streams, Kinesis, SNS). |
115+
| `dash0.faas.trigger_arn` | string | The ARN of the event source (e.g., SQS queue ARN, DynamoDB stream ARN, SNS topic ARN). |
116+
| `dash0.faas.event_bridge_source` | string | The `source` field from an EventBridge event. |
117+
| `dash0.faas.event_bridge_detail_type` | string | The `detail-type` field from an EventBridge event. |
118+
119+
#### Resource Attributes (Spans)
120+
121+
These attributes are added to the resource of span data:
122+
123+
| Attribute | Type | Description |
124+
|---|---|---|
125+
| `service.name` | string | The service name, from `OTEL_SERVICE_NAME` or defaults to `unknown_service`. |
126+
| `process.environment_variable.<KEY>` | string | Lambda environment variables (with sensitive values masked). Added to the span resource. |
127+
128+
### Log Attributes
129+
130+
The following attributes are added to log records by the extension (if relevant):
131+
132+
| Attribute | Type | Description |
133+
|---|---|---|
134+
| `faas.invocation_id` | string | The AWS request ID, used to correlate logs with the invocation span. |
135+
| `dash0.faas.payload_type` | string | The type of payload log record. Values: `lambda_event`, `lambda_return_value`, `http_request_body`, `http_response_body`. Only present on payload log records. |
136+
137+
#### Resource Attributes (Logs)
138+
139+
These attributes are added to the resource of log data:
140+
141+
| Attribute | Type | Description |
142+
|---|---|---|
143+
| `cloud.platform` | string | Always set to `aws_lambda`. |
144+
| `cloud.resource.id` | string | The full ARN of the Lambda function. |
145+
| `cloud.account.id` | string | The AWS account ID. |
146+
| `service.name` | string | The service name, from `OTEL_SERVICE_NAME` or defaults to `unknown_service`. |
147+
148+
### Metrics
149+
150+
The extension creates the following histogram metrics for each Lambda invocation:
151+
152+
| Metric | Unit | Description |
153+
|---|---|---|
154+
| `faas.duration` | ms | Duration of the invocation. |
155+
| `faas.init_duration` | ms | Duration of the cold start initialization. Only present on cold start invocations. |
156+
| `dash0.faas.billed_duration` | ms | Billed duration of the invocation. |
157+
| `dash0.faas.memory_used` | MB | Memory used by the invocation. |
158+
159+
#### Metric Attributes
160+
161+
The following attributes are added to each metric data point:
162+
163+
| Attribute | Type | Description |
164+
|---|---|---|
165+
| `cloud.resource_id` | string | The full ARN of the Lambda function. |
166+
| `cloud.account.id` | string | The AWS account ID. |
167+
168+
#### Resource Attributes (Metrics)
169+
170+
These attributes are added to the resource of metric data:
171+
172+
| Attribute | Type | Description |
173+
|---|---|---|
174+
| `service.name` | string | The service name, from `OTEL_SERVICE_NAME` or defaults to `unknown_service`. |
175+
176+
101177
## Dockerized Lambdas
102178

103179
For containerized Lambda functions, use the provided Docker images in a multi-stage build. The extension images are available for Node.js, Python, and Java runtimes.

integration-tests/tests/src/test-java-exception.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect } from 'vitest';
22
import {
33
checkLogs,
44
checkMainSpans,
5+
checkMetrics,
56
checkOverheadSpan,
67
getAttributesMap,
78
LogToCheck,
@@ -61,6 +62,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
6162
traceId,
6263
rootSpanId,
6364
});
65+
66+
await checkMetrics({
67+
functionName,
68+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
69+
});
6470
}
6571

6672
describe.concurrent('Lambda invocation', () => {

integration-tests/tests/src/test-java-success.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe } from 'vitest';
22
import {
33
checkLogs,
44
checkMainSpans,
5+
checkMetrics,
56
checkOverheadSpan,
67
LogToCheck,
78
invokeFunction,
@@ -47,6 +48,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
4748
traceId,
4849
rootSpanId,
4950
});
51+
52+
await checkMetrics({
53+
functionName,
54+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
55+
});
5056
}
5157

5258
describe.concurrent('Lambdainvocation', () => {

integration-tests/tests/src/test-node-exception.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect } from 'vitest';
22
import {
33
checkLogs,
44
checkMainSpans,
5+
checkMetrics,
56
checkOverheadSpan,
67
getAttributesMap,
78
LogToCheck,
@@ -60,6 +61,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
6061
traceId,
6162
rootSpanId,
6263
});
64+
65+
await checkMetrics({
66+
functionName,
67+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
68+
});
6369
}
6470

6571
describe.concurrent('Lambda invocation', () => {

integration-tests/tests/src/test-node-success.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkHttpSpan,
44
checkLogs,
55
checkMainSpans,
6+
checkMetrics,
67
checkOverheadSpan,
78
getAttributesMap,
89
LogToCheck,
@@ -72,6 +73,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
7273
traceId,
7374
rootSpanId,
7475
});
76+
77+
await checkMetrics({
78+
functionName,
79+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
80+
});
7581
}
7682

7783
describe.concurrent('Lambda invocation', () => {

integration-tests/tests/src/test-python-exception.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkHttpSpan,
44
checkLogs,
55
checkMainSpans,
6+
checkMetrics,
67
checkOverheadSpan,
78
getAttributesMap,
89
LogToCheck,
@@ -76,6 +77,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
7677
traceId,
7778
rootSpanId,
7879
});
80+
81+
await checkMetrics({
82+
functionName,
83+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
84+
});
7985
}
8086

8187
describe.concurrent('Lambda invocation', () => {

integration-tests/tests/src/test-python-success.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkHttpSpan,
44
checkLogs,
55
checkMainSpans,
6+
checkMetrics,
67
checkOverheadSpan,
78
getAttributesMap,
89
LogToCheck,
@@ -69,6 +70,11 @@ const verifySuccessInvocation = async (functionName: string, invocationEnd: bool
6970
traceId,
7071
rootSpanId,
7172
});
73+
74+
await checkMetrics({
75+
functionName,
76+
metricNames: ['faas.duration', 'dash0.faas.billed_duration', 'dash0.faas.memory_used', 'faas.init_duration'],
77+
});
7278
}
7379

7480
describe.concurrent('Lambdainvocation', () => {

integration-tests/tests/src/utils.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,62 @@ export const checkOverheadSpan = async ({
454454
}
455455
}
456456

457+
export const checkMetrics = async ({
458+
functionName,
459+
metricNames,
460+
}: {
461+
functionName: string;
462+
metricNames: string[];
463+
}): Promise<void> => {
464+
for (const metricName of metricNames) {
465+
let found = false;
466+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
467+
await delay(RETRY_DELAY_MS);
468+
console.log(`Attempt ${attempt} to fetch metric ${metricName} for ${functionName}`);
469+
try {
470+
const params = new URLSearchParams({
471+
dataset: DASH0_LAMBDA_TESTS_DATASET,
472+
start: 'now-10m',
473+
end: 'now',
474+
step: '1m',
475+
query: `{otel_metric_name = "${metricName}", otel_metric_type = "histogram", service_name = "${functionName}"}`,
476+
});
477+
const response = await fetch(DASH0_ENDPOINT + 'prometheus/api/v1/query_range', {
478+
method: 'POST',
479+
headers: {
480+
authorization: `Bearer ${DASH0_TOKEN}`,
481+
'content-type': 'application/x-www-form-urlencoded',
482+
},
483+
body: params.toString(),
484+
});
485+
486+
const payload = await response.json() as any;
487+
expect(payload.status).toEqual('success');
488+
489+
const results = payload.data?.result ?? [];
490+
const hasValue = results.some((r: any) =>
491+
(r.values ?? []).some((v: any) => parseFloat(v[1]) >= 1)
492+
);
493+
494+
if (hasValue) {
495+
found = true;
496+
break;
497+
}
498+
499+
if (attempt === MAX_ATTEMPTS) {
500+
throw new Error(`Metric ${metricName} for ${functionName} not found with value >= 1`);
501+
}
502+
} catch (error) {
503+
console.error(`Error fetching metric ${metricName} on attempt ${attempt}:`, error);
504+
if (attempt === MAX_ATTEMPTS) {
505+
throw error;
506+
}
507+
}
508+
}
509+
expect(found, `Metric ${metricName} should have at least one value >= 1`).toBe(true);
510+
}
511+
}
512+
457513
export const checkException = (span: any, exception_type: string) => {
458514
const events = span.events;
459515
expect(events.length).toEqual(1);

src/extension/telemetry_receiver.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::config::user::is_telemetry_log_collection_disabled;
22
use crate::otlp::exporter::{flush_telemetry_logs, send_traces};
3+
use crate::otlp::metrics_creation::create_supplementary_metrics;
34
use crate::otlp::span_creation::{create_overhead_supplementary_span, create_supplementary_spans};
45
use crate::otlp::span_mutations::build_synthetic_trace;
56
use crate::state::invocation_entry;
@@ -44,6 +45,7 @@ pub async fn telemetry(req: Request<Body>) -> Result<Response<Body>, Error> {
4445
if log.r#type == "platform.report" {
4546
if let Some(id) = &log.invocation_id {
4647
create_overhead_supplementary_span(id);
48+
create_supplementary_metrics(id);
4749
invocation_entry::update(id, |entry| {
4850
entry.state = crate::state::invocation_entry::InvocationState::Done;
4951
entry.init_duration = 0.0;

src/otlp/attributes.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// OpenTelemetry semantic convention attribute keys
2+
pub const CLOUD_RESOURCE_ID: &str = "cloud.resource_id";
3+
pub const CLOUD_ACCOUNT_ID: &str = "cloud.account.id";
4+
pub const CLOUD_PLATFORM: &str = "cloud.platform";
5+
pub const CLOUD_RESOURCE_ID_SEMCONV: &str = "cloud.resource.id";
6+
pub const SERVICE_NAME: &str = "service.name";
7+
pub const FAAS_INVOCATION_ID: &str = "faas.invocation_id";
8+
pub const FAAS_TRIGGER: &str = "faas.trigger";
9+
pub const FAAS_INIT_DURATION: &str = "faas.init_duration";
10+
11+
// Exception attributes
12+
pub const EXCEPTION_TYPE: &str = "exception.type";
13+
pub const EXCEPTION_MESSAGE: &str = "exception.message";
14+
pub const EXCEPTION_ESCAPED: &str = "exception.escaped";
15+
pub const EXCEPTION_STACKTRACE: &str = "exception.stacktrace";
16+
17+
// HTTP attributes
18+
pub const HTTP_REQUEST_BODY: &str = "http.request.body";
19+
pub const HTTP_RESPONSE_BODY: &str = "http.response.body";
20+
21+
// Dash0-specific attributes
22+
pub const DASH0_FAAS_RECORD_COUNT: &str = "dash0.faas.record_count";
23+
pub const DASH0_FAAS_TRIGGER_ARN: &str = "dash0.faas.trigger_arn";
24+
pub const DASH0_FAAS_EVENT_BRIDGE_SOURCE: &str = "dash0.faas.event_bridge_source";
25+
pub const DASH0_FAAS_EVENT_BRIDGE_DETAIL_TYPE: &str = "dash0.faas.event_bridge_detail_type";
26+
pub const DASH0_FAAS_PAYLOAD_TYPE: &str = "dash0.faas.payload_type";

0 commit comments

Comments
 (0)