Skip to content

Commit 61ba8b8

Browse files
Lms24claude
andcommitted
feat(aws-serverless): Emit low cardinality function.aws span names
`function.aws` spans were already named after the Lambda function, which is what the `{{faas.name}}` template in the Sentry span name conventions asks for, so their names do not change. What was missing is the conventions' static fallback: when the invocation context carries no function name, the span was started with an empty name and `faas.name` was left unset. Resolve the function name from `context.functionName` or the `AWS_LAMBDA_FUNCTION_NAME` environment variable, use it for both the span name and `faas.name`, and fall back to `Serverless function execution` under span streaming. Without span streaming the name stays byte-identical to before. Refs #23954 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8ca1bf7 commit 61ba8b8

3 files changed

Lines changed: 147 additions & 3 deletions

File tree

MIGRATION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,7 @@ The following span names were adjusted:
965965
| `router` | Framework-specific, sometimes containing the raw URL | `/users/123`, `SvelteKit Route Change` | The span's `http.route`, or `Router` if the SDK has none | `/users/:id`, `Router` |
966966
| `handler` | Framework-specific, often carrying the request method | `GET /users/:id`, `route-handler`, `getUser` | The span's `http.route`, or `Request handler` if the SDK has none | `/users/:id`, `Request handler` |
967967
| `function.gcp` | The request method and path for HTTP functions, otherwise the trigger's event or trigger type | `POST /users`, `google.pubsub.topic.publish`, `firebase.function.http.request` | The function name, or `Serverless function execution` if the SDK cannot resolve one | `myFunction`, `Serverless function execution` |
968+
| `function.aws` | The Lambda function name | `my-function` | Unchanged, except that the SDK now falls back to `Serverless function execution` if it cannot resolve the function name | `my-function`, `Serverless function execution` |
968969
| `graphql` | The graphql phase and, for operations, the operation name | `query GetUser`, `graphql.parse`, `graphql.resolve user.0.name` | The operation type, or the processing type where there is none | `GraphQL query`, `GraphQL parse`, `GraphQL resolve` |
969970
| `gen_ai.chat`, `gen_ai.embeddings`, `gen_ai.generate_content` | `{operation} {model}`, or `{operation} unknown` if the model is missing | `chat gpt-4`, `chat unknown` | `{operation} {model}`, or `{operation}` if the model is missing | `chat gpt-4`, `chat` |
970971
| `gen_ai.invoke_agent` | The LangChain chain name, prefixed with `chain` rather than the operation | `chain format_prompt`, `chain unknown_chain` | `{operation} {name}`, where the name is the span's `gen_ai.agent.name`, `gen_ai.pipeline.name` or `gen_ai.function_id`, in that order, or `{operation}` if the span carries none | `invoke_agent format_prompt`, `invoke_agent` |
@@ -992,6 +993,13 @@ Whatever the name no longer carries stays on the span as an attribute:
992993
- `gcp.function.context.*` — the fields of the trigger event, including the event type the span used to be named after.
993994
- `http.request.method` and `url.path` — for HTTP-triggered functions, the method and path the span used to be named after.
994995

996+
`function.aws` spans in `@sentry/aws-serverless` were already named after the Lambda function, so
997+
their names are unchanged. The only new behaviour is the fallback: if neither the invocation context
998+
nor the `AWS_LAMBDA_FUNCTION_NAME` environment variable yields a function name, the span is named
999+
`Serverless function execution` instead of carrying an empty name. These spans continue to carry the
1000+
function name on `faas.name`, the request URL on `url.full`, and the invocation details on
1001+
`aws.lambda.*` and `aws.cloudwatch.logs.*`.
1002+
9951003
#### Filtering and sampling
9961004

9971005
When span streaming is enabled (i.e. by default) `ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name:

packages/aws-serverless/src/requestSpanOptions.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ import {
2828
} from '@sentry/conventions/attributes';
2929
import { FUNCTION_AWS } from '@sentry/conventions/op';
3030
import type { SpanAttributes, StartSpanOptions } from '@sentry/core';
31-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl } from '@sentry/core';
31+
import {
32+
getClient,
33+
hasSpanStreamingEnabled,
34+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
35+
SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK,
36+
filterCollectedUrl,
37+
} from '@sentry/core';
3238
import type { Context } from 'aws-lambda';
3339
import { ATTR_FAAS_EXECUTION, ATTR_FAAS_ID } from './semconv';
3440

@@ -43,9 +49,16 @@ interface ApiGatewayLikeEvent {
4349
* Builds the options for the `function.aws` transaction started for each invocation.
4450
*/
4551
export function getRequestSpanOptions(event: unknown, context: Context, requestIsColdStart: boolean): StartSpanOptions {
52+
const client = getClient();
53+
54+
const functionName = getFunctionName(context);
55+
4656
// The span is started within the surrounding `continueTrace`, so it continues the incoming trace.
4757
return {
48-
name: context.functionName,
58+
name:
59+
client && hasSpanStreamingEnabled(client)
60+
? functionName || SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK
61+
: context.functionName,
4962
attributes: {
5063
[SENTRY_OP]: FUNCTION_AWS,
5164
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.aws_lambda',
@@ -55,13 +68,23 @@ export function getRequestSpanOptions(event: unknown, context: Context, requestI
5568
[CLOUD_ACCOUNT_ID]: extractAccountId(context.invokedFunctionArn),
5669
[CLOUD_PROVIDER]: 'aws',
5770
[CLOUD_PLATFORM]: 'aws_lambda',
58-
[FAAS_NAME]: context.functionName,
71+
[FAAS_NAME]: functionName,
5972
[FAAS_COLDSTART]: requestIsColdStart,
6073
...extractOtherEventFields(event),
6174
},
6275
};
6376
}
6477

78+
/**
79+
* Resolves the name of the currently executing Lambda function.
80+
*
81+
* The runtime always populates `context.functionName`; `AWS_LAMBDA_FUNCTION_NAME` covers custom
82+
* runtimes and local emulators that only partially fill in the invocation context.
83+
*/
84+
function getFunctionName(context: Context): string | undefined {
85+
return context.functionName || process.env.AWS_LAMBDA_FUNCTION_NAME || undefined;
86+
}
87+
6588
function extractAccountId(arn: string): string | undefined {
6689
const parts = arn.split(':');
6790
if (parts.length >= 5) {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
CLOUD_ACCOUNT_ID,
3+
CLOUD_PLATFORM,
4+
CLOUD_PROVIDER,
5+
FAAS_COLDSTART,
6+
FAAS_NAME,
7+
SENTRY_KIND,
8+
SENTRY_OP,
9+
URL_FULL,
10+
} from '@sentry/conventions/attributes';
11+
import { FUNCTION_AWS } from '@sentry/conventions/op';
12+
import type { Client } from '@sentry/core';
13+
import * as SentryCore from '@sentry/core';
14+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core';
15+
import type { Context } from 'aws-lambda';
16+
import { afterEach, describe, expect, test, vi } from 'vitest';
17+
import { getRequestSpanOptions } from '../src/requestSpanOptions';
18+
19+
function createContext(overrides: Partial<Context> = {}): Context {
20+
return {
21+
functionName: 'my-function',
22+
functionVersion: '$LATEST',
23+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:012345678912:function:my-function',
24+
awsRequestId: '1e1cd0dc-6bd0-4e0e-9a5d-63e8c7bd4b3b',
25+
...overrides,
26+
} as Context;
27+
}
28+
29+
function mockSpanStreaming(enabled: boolean): void {
30+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
31+
getOptions: () => ({ traceLifecycle: enabled ? 'stream' : 'static' }),
32+
} as unknown as Client);
33+
}
34+
35+
describe('getRequestSpanOptions', () => {
36+
afterEach(() => {
37+
vi.restoreAllMocks();
38+
vi.unstubAllEnvs();
39+
});
40+
41+
test('names the span after the function and sets the invocation attributes', () => {
42+
mockSpanStreaming(false);
43+
44+
expect(getRequestSpanOptions({}, createContext(), true)).toEqual({
45+
name: 'my-function',
46+
attributes: {
47+
[SENTRY_OP]: FUNCTION_AWS,
48+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.aws_lambda',
49+
[SENTRY_KIND]: 'server',
50+
'faas.execution': '1e1cd0dc-6bd0-4e0e-9a5d-63e8c7bd4b3b',
51+
'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:my-function',
52+
[CLOUD_ACCOUNT_ID]: '012345678912',
53+
[CLOUD_PROVIDER]: 'aws',
54+
[CLOUD_PLATFORM]: 'aws_lambda',
55+
[FAAS_NAME]: 'my-function',
56+
[FAAS_COLDSTART]: true,
57+
},
58+
});
59+
});
60+
61+
test.each([true, false])('names the span after the function with span streaming %s', spanStreamingEnabled => {
62+
mockSpanStreaming(spanStreamingEnabled);
63+
64+
const spanOptions = getRequestSpanOptions({}, createContext(), false);
65+
66+
expect(spanOptions.name).toBe('my-function');
67+
expect(spanOptions.attributes?.[FAAS_NAME]).toBe('my-function');
68+
});
69+
70+
test('keeps the API gateway URL on an attribute rather than in the name', () => {
71+
mockSpanStreaming(true);
72+
73+
const event = {
74+
headers: { host: 'api.example.com', 'x-forwarded-proto': 'https' },
75+
path: '/users/123',
76+
queryStringParameters: { expand: 'profile' },
77+
};
78+
79+
const spanOptions = getRequestSpanOptions(event, createContext(), false);
80+
81+
expect(spanOptions.name).toBe('my-function');
82+
expect(spanOptions.attributes?.[URL_FULL]).toBe('https://api.example.com/users/123?expand=profile');
83+
});
84+
85+
test('falls back to AWS_LAMBDA_FUNCTION_NAME when the context has no function name', () => {
86+
mockSpanStreaming(true);
87+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', 'my-env-function');
88+
89+
const spanOptions = getRequestSpanOptions({}, createContext({ functionName: '' }), false);
90+
91+
expect(spanOptions.name).toBe('my-env-function');
92+
expect(spanOptions.attributes?.[FAAS_NAME]).toBe('my-env-function');
93+
});
94+
95+
test('falls back to the static span name when no function name is resolvable', () => {
96+
mockSpanStreaming(true);
97+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', '');
98+
99+
const spanOptions = getRequestSpanOptions({}, createContext({ functionName: '' }), false);
100+
101+
expect(spanOptions.name).toBe(SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK);
102+
expect(spanOptions.attributes?.[FAAS_NAME]).toBeUndefined();
103+
});
104+
105+
test('keeps the unresolved function name without span streaming', () => {
106+
mockSpanStreaming(false);
107+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', '');
108+
109+
const spanOptions = getRequestSpanOptions({}, createContext({ functionName: '' }), false);
110+
111+
expect(spanOptions.name).toBe('');
112+
});
113+
});

0 commit comments

Comments
 (0)