Skip to content

Commit aad6770

Browse files
committed
wip
1 parent 1854214 commit aad6770

File tree

6 files changed

+349
-1
lines changed

6 files changed

+349
-1
lines changed

packages/aws-serverless/rollup.npm.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default [
66
// TODO: `awslambda-auto.ts` is a file which the lambda layer uses to automatically init the SDK. Does it need to be
77
// in the npm package? Is it possible that some people are using it themselves in the same way the layer uses it (in
88
// which case removing it would be a breaking change)? Should it stay here or not?
9-
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
9+
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts', 'src/sentry-extension/index.ts'],
1010
// packages with bundles have a different build directory structure
1111
hasBundles: true,
1212
packageSpecificConfig: {

packages/aws-serverless/scripts/buildLambdaLayer.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ async function buildLambdaLayer(): Promise<void> {
4545

4646
replaceSDKSource();
4747

48+
fsForceMkdirSync('./build/aws/dist-serverless/extensions');
49+
fsForceMkdirSync('./build/aws/dist-serverless/sentry-extension');
50+
fs.renameSync(
51+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/sentry-extension',
52+
'./build/aws/dist-serverless/sentry-extension',
53+
);
54+
fs.copyFileSync('./src/extensions/sentry-extension', './build/aws/dist-serverless/extensions/sentry-extension');
55+
fs.chmodSync('./build/aws/dist-serverless/extensions/sentry-extension', 0o755);
56+
4857
const zipFilename = `sentry-node-serverless-${version}.zip`;
4958
console.log(`Creating final layer zip file ${zipFilename}.`);
5059
// need to preserve the symlink above with -y
@@ -72,6 +81,7 @@ async function pruneNodeModules(): Promise<void> {
7281
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js',
7382
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/awslambda-auto.js',
7483
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/awslambda-auto.js',
84+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/sentry-extension/index.js',
7585
];
7686

7787
const { fileList } = await nodeFileTrace(entrypoints);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
# SPDX-License-Identifier: MIT-0
4+
5+
set -euo pipefail
6+
7+
OWN_FILENAME="$(basename $0)"
8+
LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename
9+
10+
echo "${LAMBDA_EXTENSION_NAME} launching extension"
11+
unset NODE_OPTIONS
12+
exec "/opt/${LAMBDA_EXTENSION_NAME}/index.js"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* The Extension API Client.
3+
*/
4+
export class ExtensionApiClient {
5+
private readonly _baseUrl: string;
6+
7+
public constructor() {
8+
this._baseUrl = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension`;
9+
}
10+
11+
/**
12+
* Register this extension as an external extension with AWS.
13+
* @returns The extension identifier, or null if the extension was not registered.
14+
*/
15+
public async register(): Promise<string | null> {
16+
const res = await fetch(`${this._baseUrl}/register`, {
17+
method: 'POST',
18+
body: JSON.stringify({
19+
events: ['INVOKE', 'SHUTDOWN'],
20+
}),
21+
headers: {
22+
'Content-Type': 'application/json',
23+
'Lambda-Extension-Name': 'sentry-extension',
24+
},
25+
});
26+
27+
if (!res.ok) {
28+
throw new Error(`Failed to register with the extension API: ${await res.text()}`);
29+
}
30+
31+
return res.headers.get('lambda-extension-identifier');
32+
}
33+
34+
/**
35+
* Advances the extension to the next event.
36+
* @param extensionId The extension identifier.
37+
* @returns The event data, or null if the extension is not ready to process events.
38+
*/
39+
public async next(extensionId: string): Promise<any> {
40+
const res = await fetch(`${this._baseUrl}/event/next`, {
41+
headers: {
42+
'Lambda-Extension-Identifier': extensionId,
43+
'Content-Type': 'application/json',
44+
},
45+
});
46+
47+
if (!res.ok) {
48+
throw new Error(`Failed to advance to next event: ${await res.text()}`);
49+
}
50+
51+
return res.json();
52+
}
53+
54+
/**
55+
* Reports an error to the extension API.
56+
* @param extensionId The extension identifier.
57+
* @param phase The phase of the extension.
58+
* @param err The error to report.
59+
*/
60+
public async error(extensionId: string, phase: string, err: Error): Promise<never> {
61+
const errorType = `Extension.${err.name || 'UnknownError'}`;
62+
63+
const res = await fetch(`${this._baseUrl}/${phase}/error`, {
64+
method: 'POST',
65+
body: JSON.stringify({
66+
errorMessage: err.message || err.toString(),
67+
errorType,
68+
stackTrace: [err.stack],
69+
}),
70+
headers: {
71+
'Content-Type': 'application/json',
72+
'Lambda-Extension-Identifier': extensionId,
73+
'Lambda-Extension-Function-Error': errorType,
74+
},
75+
});
76+
77+
if (!res.ok) {
78+
throw new Error(`Failed to report error: ${await res.text()}`);
79+
}
80+
81+
throw err;
82+
}
83+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env node
2+
import * as http from 'node:http';
3+
import { buffer } from 'node:stream/consumers';
4+
import { ExtensionApiClient } from './extensions-api';
5+
import { TelemetryApiClient, createTelemetryListener } from './telemetry-api';
6+
7+
const EventType = {
8+
INVOKE: 'INVOKE',
9+
SHUTDOWN: 'SHUTDOWN',
10+
} as const;
11+
12+
interface ExtensionEvent {
13+
eventType: string;
14+
[key: string]: any;
15+
}
16+
17+
function handleShutdown(event: any): void {
18+
console.log('shutdown', { event });
19+
// process.exit(0);
20+
}
21+
22+
function handleInvoke(event: any): void {
23+
console.log('invoke', { event });
24+
// process.exit(0);
25+
}
26+
27+
async function main(): Promise<void> {
28+
process.on('SIGINT', () => handleShutdown('SIGINT'));
29+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
30+
31+
console.log('hello from extension');
32+
console.log('register');
33+
const api = new ExtensionApiClient();
34+
const extensionId = await api.register();
35+
36+
if (!extensionId) {
37+
console.error('Failed to register with the extension API');
38+
process.exit(1);
39+
}
40+
41+
// Set up telemetry listener and subscription
42+
console.log('Setting up telemetry listener');
43+
const telemetryPort = 8080;
44+
const { uri: telemetryUri, ready: telemetryReady } = createTelemetryListener(telemetryPort);
45+
46+
// Wait for telemetry listener to be ready
47+
await telemetryReady;
48+
49+
// Subscribe to telemetry streams
50+
console.log('Subscribing to telemetry streams');
51+
const telemetryApi = new TelemetryApiClient();
52+
await telemetryApi.subscribe(extensionId, telemetryUri);
53+
54+
const server = http.createServer(async (req, res) => {
55+
if (req.url?.startsWith('/envelope')) {
56+
try {
57+
const buf = await buffer(req);
58+
const envelopeBytes = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
59+
const envelope = new TextDecoder().decode(envelopeBytes);
60+
const piece = envelope.split('\n')[0];
61+
const header = JSON.parse(piece ?? '{}') as { dsn?: string };
62+
const dsn = new URL(header.dsn || '');
63+
const projectId = dsn.pathname.replace('/', '');
64+
const upstreamSentryUrl = `https://${dsn.hostname}/api/${projectId}/envelope/`;
65+
66+
console.log('tunneling to sentry', { upstreamSentryUrl });
67+
console.log('headers', req.headers);
68+
69+
fetch(upstreamSentryUrl, {
70+
method: 'POST',
71+
body: envelopeBytes,
72+
}).catch(err => {
73+
console.error('error tunneling to sentry', err);
74+
});
75+
76+
res.writeHead(200, { 'Content-Type': 'application/json' });
77+
res.end(JSON.stringify({}));
78+
} catch (e) {
79+
console.error('error tunneling to sentry', e);
80+
res.writeHead(500, { 'Content-Type': 'application/json' });
81+
res.end(JSON.stringify({ error: 'error tunneling to sentry' }));
82+
}
83+
}
84+
});
85+
86+
server.listen(9000, () => {
87+
console.log('server listening on port 9000');
88+
});
89+
90+
while (true) {
91+
console.log('next');
92+
const event: ExtensionEvent = await api.next(extensionId);
93+
94+
try {
95+
switch (event.eventType) {
96+
case EventType.SHUTDOWN:
97+
handleShutdown(event);
98+
break;
99+
case EventType.INVOKE:
100+
handleInvoke(event);
101+
break;
102+
default:
103+
throw new Error(`Unknown event type: ${event.eventType}`);
104+
}
105+
} catch (err) {
106+
await api.error(extensionId, 'exit', err as Error);
107+
}
108+
}
109+
}
110+
111+
main().catch(err => {
112+
console.error('Fatal error:', err);
113+
// process.exit(1);
114+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as http from 'node:http';
2+
3+
/**
4+
* The Telemetry API Client for subscribing to Lambda telemetry events.
5+
*/
6+
export class TelemetryApiClient {
7+
private readonly _baseUrl: string;
8+
9+
public constructor() {
10+
this._baseUrl = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2022-07-01/telemetry`;
11+
}
12+
13+
/**
14+
* Subscribe to telemetry streams.
15+
* @param extensionId The extension identifier from the Extensions API registration.
16+
* @param listenerUri The URI where the telemetry listener is running.
17+
* @returns Promise that resolves when subscription is successful.
18+
*/
19+
public async subscribe(extensionId: string, listenerUri: string): Promise<void> {
20+
const subscriptionRequest = {
21+
schemaVersion: '2022-12-13',
22+
types: [
23+
'platform', // Platform telemetry (logs, metrics, traces)
24+
'function', // Function logs
25+
'extension', // Extension logs
26+
],
27+
buffering: {
28+
maxItems: 1000,
29+
maxBytes: 256 * 1024, // 256KB
30+
timeoutMs: 100,
31+
},
32+
destination: {
33+
protocol: 'HTTP',
34+
URI: listenerUri,
35+
},
36+
};
37+
38+
const res = await fetch(this._baseUrl, {
39+
method: 'PUT',
40+
body: JSON.stringify(subscriptionRequest),
41+
headers: {
42+
'Content-Type': 'application/json',
43+
'Lambda-Extension-Identifier': extensionId,
44+
},
45+
});
46+
47+
if (!res.ok) {
48+
throw new Error(`Failed to subscribe to telemetry API: ${await res.text()}`);
49+
}
50+
51+
console.log('Successfully subscribed to telemetry streams');
52+
}
53+
}
54+
55+
/**
56+
* Telemetry event types based on AWS Lambda Telemetry API schema.
57+
*/
58+
export interface TelemetryEvent {
59+
time: string;
60+
type: string;
61+
record: any;
62+
}
63+
64+
/**
65+
* Creates and starts an HTTP telemetry listener.
66+
* @param port The port to listen on for telemetry events.
67+
* @returns The listener URI and a promise that resolves when the server is ready.
68+
*/
69+
export function createTelemetryListener(port: number = 8080): { uri: string; ready: Promise<void> } {
70+
const server = http.createServer((req, res) => {
71+
if (req.method === 'POST') {
72+
let body = '';
73+
74+
req.on('data', chunk => {
75+
body += chunk.toString();
76+
});
77+
78+
req.on('end', () => {
79+
try {
80+
const events: TelemetryEvent[] = JSON.parse(body);
81+
82+
// Log all telemetry events
83+
for (const event of events) {
84+
if (event.type !== 'extension') {
85+
console.log('Telemetry Event:', {
86+
time: event.time,
87+
type: event.type,
88+
record: event.record,
89+
});
90+
}
91+
}
92+
93+
// Send successful response
94+
res.writeHead(200, { 'Content-Type': 'application/json' });
95+
res.end('OK');
96+
} catch (error) {
97+
console.error('Error processing telemetry events:', error);
98+
res.writeHead(500, { 'Content-Type': 'application/json' });
99+
res.end(JSON.stringify({ error: 'Failed to process telemetry events' }));
100+
}
101+
});
102+
103+
req.on('error', error => {
104+
console.error('Error receiving telemetry data:', error);
105+
res.writeHead(500, { 'Content-Type': 'application/json' });
106+
res.end(JSON.stringify({ error: 'Failed to receive telemetry data' }));
107+
});
108+
} else {
109+
res.writeHead(404, { 'Content-Type': 'application/json' });
110+
res.end(JSON.stringify({ error: 'Not found' }));
111+
}
112+
});
113+
114+
const uri = `http://sandbox.localdomain:${port}`;
115+
116+
const ready = new Promise<void>((resolve, reject) => {
117+
server.listen(port, 'sandbox.localdomain', () => {
118+
console.log(`Telemetry listener started on ${uri}`);
119+
resolve();
120+
});
121+
122+
server.on('error', error => {
123+
console.error('Telemetry listener error:', error);
124+
reject(error);
125+
});
126+
});
127+
128+
return { uri, ready };
129+
}

0 commit comments

Comments
 (0)