Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion packages/aws-serverless/src/integration/awslambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import {
defineIntegration,
getCurrentScope,
safeSetSpanJSONAttributes,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
} from '@sentry/core';
import { captureException, generateInstrumentOnce } from '@sentry/node';
import { eventContextExtractor, markEventUnhandled } from '../utils';
import { AwsLambdaInstrumentation } from './instrumentation-aws-lambda/instrumentation';
Expand Down Expand Up @@ -36,12 +42,50 @@ export const instrumentAwsLambda = generateInstrumentOnce(
},
);

const AWS_LAMBDA_CONTEXT_FIELDS = [
'aws_request_id',
'function_name',
'function_version',
'invoked_function_arn',
'execution_duration_in_millis',
'remaining_time_in_millis',
] as const;

const AWS_CLOUDWATCH_CONTEXT_FIELDS = ['log_group', 'log_stream', 'url'] as const;

const _awsLambdaIntegration = ((options: AwsLambdaOptions = {}) => {
return {
name: 'AwsLambda',
setupOnce() {
instrumentAwsLambda(options);
},
processSegmentSpan(span) {
const { contexts } = getCurrentScope().getScopeData();

const awsLambda = contexts['aws.lambda'];
if (awsLambda) {
const attrs: Record<string, unknown> = {};
for (const field of AWS_LAMBDA_CONTEXT_FIELDS) {
const value = awsLambda[field];
if (typeof value === 'string' || typeof value === 'number') {
attrs[`aws.lambda.${field}`] = value;
}
}
safeSetSpanJSONAttributes(span, attrs);
}

const awsCloudwatch = contexts['aws.cloudwatch.logs'];
if (awsCloudwatch) {
const attrs: Record<string, unknown> = {};
for (const field of AWS_CLOUDWATCH_CONTEXT_FIELDS) {
const value = awsCloudwatch[field];
if (typeof value === 'string' || typeof value === 'number') {
attrs[`aws.cloudwatch.logs.${field}`] = value;
}
}
safeSetSpanJSONAttributes(span, attrs);
}
},
};
}) satisfies IntegrationFn;

Expand Down
104 changes: 104 additions & 0 deletions packages/aws-serverless/test/awslambda-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { StreamedSpanJSON } from '@sentry/core';
import { describe, expect, test, vi } from 'vitest';
import { awsLambdaIntegration } from '../src/integration/awslambda';

const mockGetScopeData = vi.fn();

vi.mock('@sentry/core', async () => {
const original = await vi.importActual('@sentry/core');
return {
...original,
getCurrentScope: () => ({
getScopeData: mockGetScopeData,
}),
};
});

vi.mock('@sentry/node', async () => {
const original = await vi.importActual('@sentry/node');
return {
...original,
generateInstrumentOnce: () => () => {},
};
});

describe('awsLambdaIntegration processSegmentSpan', () => {
function makeSpanJSON(): StreamedSpanJSON {
return {
name: 'test',
span_id: 'abc',
trace_id: 'def',
start_timestamp: 0,
end_timestamp: 1,
status: 'ok',
is_segment: true,
attributes: {},
};
}

test('maps aws.lambda context fields to segment span attributes', () => {
mockGetScopeData.mockReturnValue({
contexts: {
'aws.lambda': {
aws_request_id: 'req-123',
function_name: 'my-function',
function_version: '$LATEST',
invoked_function_arn: 'arn:aws:lambda:us-east-1:123:function:my-function',
execution_duration_in_millis: 150,
remaining_time_in_millis: 2850,
'sys.argv': ['/usr/bin/node', '--secret=abc'],
},
},
});

const integration = awsLambdaIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual(
expect.objectContaining({
'aws.lambda.aws_request_id': 'req-123',
'aws.lambda.function_name': 'my-function',
'aws.lambda.function_version': '$LATEST',
'aws.lambda.invoked_function_arn': 'arn:aws:lambda:us-east-1:123:function:my-function',
'aws.lambda.execution_duration_in_millis': 150,
'aws.lambda.remaining_time_in_millis': 2850,
}),
);
expect(span.attributes).not.toHaveProperty('aws.lambda.sys.argv');
});

test('maps aws.cloudwatch.logs context fields to segment span attributes', () => {
mockGetScopeData.mockReturnValue({
contexts: {
'aws.cloudwatch.logs': {
log_group: '/aws/lambda/my-function',
log_stream: '2024/01/01/[$LATEST]abc123',
url: 'https://console.aws.amazon.com/cloudwatch/home',
},
},
});

const integration = awsLambdaIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual(
expect.objectContaining({
'aws.cloudwatch.logs.log_group': '/aws/lambda/my-function',
'aws.cloudwatch.logs.log_stream': '2024/01/01/[$LATEST]abc123',
'aws.cloudwatch.logs.url': 'https://console.aws.amazon.com/cloudwatch/home',
}),
);
});

test('does nothing when no aws contexts are set', () => {
mockGetScopeData.mockReturnValue({ contexts: {} });

const integration = awsLambdaIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual({});
});
});
Comment thread
chargome marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, getCurrentScope, safeSetSpanJSONAttributes } from '@sentry/core';

const GCP_CONTEXT_ATTRIBUTE_MAP: Record<string, string> = {
type: 'gcp.function.context.type',
source: 'gcp.function.context.source',
id: 'gcp.function.context.id',
specversion: 'gcp.function.context.specversion',
time: 'gcp.function.context.time',
eventId: 'gcp.function.context.event_id',
timestamp: 'gcp.function.context.timestamp',
eventType: 'gcp.function.context.event_type',
resource: 'gcp.function.context.resource',
};

const _gcpContextIntegration = (() => {
return {
name: 'GcpContext',
processSegmentSpan(span) {
const gcpContext = getCurrentScope().getScopeData().contexts['gcp.function.context'];
if (!gcpContext) {
return;
}

const attrs: Record<string, unknown> = {};
for (const [field, attrName] of Object.entries(GCP_CONTEXT_ATTRIBUTE_MAP)) {
const value = gcpContext[field];
if (typeof value === 'string' || typeof value === 'number') {
attrs[attrName] = value;
}
}
safeSetSpanJSONAttributes(span, attrs);
},
};
}) satisfies IntegrationFn;

export const gcpContextIntegration = defineIntegration(_gcpContextIntegration);
3 changes: 2 additions & 1 deletion packages/google-cloud-serverless/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrationsWithoutPerformance, init as initNode } from '@sentry/node';
import { isCjs } from '@sentry/node-core';
import { gcpContextIntegration } from './integrations/gcp-context';
import { googleCloudGrpcIntegration } from './integrations/google-cloud-grpc';
import { googleCloudHttpIntegration } from './integrations/google-cloud-http';

Expand All @@ -17,7 +18,7 @@ function getCjsOnlyIntegrations(): Integration[] {

/** Get the default integrations for the GCP SDK. */
export function getDefaultIntegrations(_options: Options): Integration[] {
return [...getDefaultIntegrationsWithoutPerformance(), ...getCjsOnlyIntegrations()];
return [...getDefaultIntegrationsWithoutPerformance(), gcpContextIntegration(), ...getCjsOnlyIntegrations()];
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { StreamedSpanJSON } from '@sentry/core';
import { describe, expect, test, vi } from 'vitest';
import { gcpContextIntegration } from '../../src/integrations/gcp-context';

const mockGetScopeData = vi.fn();

vi.mock('@sentry/core', async () => {
const original = await vi.importActual('@sentry/core');
return {
...original,
getCurrentScope: () => ({
getScopeData: mockGetScopeData,
}),
};
});

describe('gcpContextIntegration', () => {
function makeSpanJSON(): StreamedSpanJSON {
return {
name: 'test',
span_id: 'abc',
trace_id: 'def',
start_timestamp: 0,
end_timestamp: 1,
status: 'ok',
is_segment: true,
attributes: {},
};
}

test('maps CloudEvents context fields to segment span attributes', () => {
mockGetScopeData.mockReturnValue({
contexts: {
'gcp.function.context': {
type: 'google.cloud.pubsub.topic.v1.messagePublished',
source: '//pubsub.googleapis.com/projects/my-project/topics/my-topic',
id: 'evt-123',
specversion: '1.0',
time: '2024-01-01T00:00:00Z',
},
},
});

const integration = gcpContextIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual(
expect.objectContaining({
'gcp.function.context.type': 'google.cloud.pubsub.topic.v1.messagePublished',
'gcp.function.context.source': '//pubsub.googleapis.com/projects/my-project/topics/my-topic',
'gcp.function.context.id': 'evt-123',
'gcp.function.context.specversion': '1.0',
'gcp.function.context.time': '2024-01-01T00:00:00Z',
}),
);
});

test('maps legacy CloudFunctions fields with snake_case attribute names', () => {
mockGetScopeData.mockReturnValue({
contexts: {
'gcp.function.context': {
eventId: 'evt-456',
timestamp: '2024-01-01T00:00:00Z',
eventType: 'providers/cloud.pubsub/eventTypes/topic.publish',
resource: 'projects/my-project/topics/my-topic',
},
},
});

const integration = gcpContextIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual(
expect.objectContaining({
'gcp.function.context.event_id': 'evt-456',
'gcp.function.context.timestamp': '2024-01-01T00:00:00Z',
'gcp.function.context.event_type': 'providers/cloud.pubsub/eventTypes/topic.publish',
'gcp.function.context.resource': 'projects/my-project/topics/my-topic',
}),
);
});

test('skips non-string values', () => {
mockGetScopeData.mockReturnValue({
contexts: {
'gcp.function.context': {
type: 'some.event',
resource: { service: 'pubsub', name: 'my-topic' },
data: { payload: 'secret' },
},
},
});

const integration = gcpContextIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual(
expect.objectContaining({
'gcp.function.context.type': 'some.event',
}),
);
expect(span.attributes).not.toHaveProperty('gcp.function.context.resource');
expect(span.attributes).not.toHaveProperty('gcp.function.context.data');
});

test('does nothing when no gcp context is set', () => {
mockGetScopeData.mockReturnValue({ contexts: {} });

const integration = gcpContextIntegration();
const span = makeSpanJSON();
integration.processSegmentSpan!(span, {} as any);

expect(span.attributes).toEqual({});
});
});
Loading