@allthings/cloud-toolkit 5.4.0-beta.1 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,7 +125,7 @@ Global `fetch` (undici) and `http`/`https` calls are traced automatically.
125
125
  Configuration (environment):
126
126
 
127
127
  - `OTEL_ENABLED=true` — turns tracing on; otherwise every function is a pass-through.
128
- - Lambda `tracing: Active` — provides `AWS_XRAY_DAEMON_ADDRESS`; spans go there as OTLP over UDP (no layer, no added latency). Without it, spans go via OTLP/HTTP to `OTEL_EXPORTER_OTLP_*` (default `localhost:4318`).
128
+ - Lambda `tracing: Active` — provides `AWS_XRAY_DAEMON_ADDRESS`; spans go there as OTLP over UDP (no layer; a send only waits for the local socket write, and `withInvocationSpan` waits at most 2.5 s for the flush before returning). Without it, spans go via OTLP/HTTP to `OTEL_EXPORTER_OTLP_*` (default `localhost:4318`).
129
129
  - `OTEL_SERVICE_NAME` — optional, defaults to `AWS_LAMBDA_FUNCTION_NAME`.
130
130
 
131
131
  The SDK starts when the module is first imported, so import it before modules that load `http`/`https`.
@@ -1,2 +1,2 @@
1
- export { createOtlpUdpSpanExporter } from './otlpUdpSpanExporter';
1
+ export { createOtlpUdpSpanExporter, IOtlpUdpSpanExporterOptions, } from './otlpUdpSpanExporter';
2
2
  export { createTracingSdk, ITracingSdk, ITracingSdkOptions, TSpanAttributes, withInvocationSpan, withStageSpan, } from './tracing';
@@ -1,2 +1,5 @@
1
1
  import { SpanExporter } from '@opentelemetry/sdk-trace-node';
2
- export declare const createOtlpUdpSpanExporter: (endpoint?: string) => SpanExporter;
2
+ export interface IOtlpUdpSpanExporterOptions {
3
+ maxPayloadBytes?: number;
4
+ }
5
+ export declare const createOtlpUdpSpanExporter: (endpoint?: string, { maxPayloadBytes }?: IOtlpUdpSpanExporterOptions) => SpanExporter;
@@ -18,6 +18,7 @@ const DEFAULT_ENDPOINT = '127.0.0.1:2000';
18
18
  const PROTOCOL_HEADER = '{"format":"json","version":1}\n';
19
19
  const OTEL_TRACES_BINARY_PREFIX = 'T1S';
20
20
  const MAX_PORT = 65_535;
21
+ const MAX_UDP_PAYLOAD_BYTES = 65_507;
21
22
  const parseEndpoint = (endpoint) => {
22
23
  const [host, port] = endpoint.split(':');
23
24
  const parsedPort = Number(port);
@@ -30,37 +31,63 @@ const parseEndpoint = (endpoint) => {
30
31
  }
31
32
  return { host, port: parsedPort };
32
33
  };
33
- const reportFailure = (error, resultCallback) => {
34
- api.diag.error('Error sending OTLP/UDP data: %s', error);
35
- resultCallback({ code: core.ExportResultCode.FAILED, error });
34
+ const encode = (spans) => {
35
+ const serialized = otlpTransformer.ProtobufTraceSerializer.serializeRequest(spans);
36
+ return serialized == null
37
+ ? undefined
38
+ : Buffer.from(`${PROTOCOL_HEADER}${OTEL_TRACES_BINARY_PREFIX}${Buffer.from(serialized).toString('base64')}`, 'utf8');
36
39
  };
37
- const sendMessage = (socket, message, { host, port }, resultCallback) => {
40
+ const encodeBatches = (spans, maxPayloadBytes) => {
41
+ if (spans.length === 0) {
42
+ return { droppedSpans: 0, messages: [] };
43
+ }
44
+ const message = encode(spans);
45
+ if (!message) {
46
+ return { droppedSpans: 0, messages: [] };
47
+ }
48
+ if (message.length <= maxPayloadBytes) {
49
+ return { droppedSpans: 0, messages: [message] };
50
+ }
51
+ if (spans.length === 1) {
52
+ return { droppedSpans: 1, messages: [] };
53
+ }
54
+ const middle = Math.ceil(spans.length / 2);
55
+ const head = encodeBatches(spans.slice(0, middle), maxPayloadBytes);
56
+ const tail = encodeBatches(spans.slice(middle), maxPayloadBytes);
57
+ return {
58
+ droppedSpans: head.droppedSpans + tail.droppedSpans,
59
+ messages: [...head.messages, ...tail.messages],
60
+ };
61
+ };
62
+ const send = (socket, message, { host, port }) => new Promise((resolve) => {
38
63
  try {
39
- socket.send(Buffer.from(message, 'utf8'), port, host, (error) => {
40
- if (error) {
41
- reportFailure(error, resultCallback);
42
- return;
43
- }
44
- resultCallback({ code: core.ExportResultCode.SUCCESS });
64
+ socket.send(message, port, host, (error) => {
65
+ resolve(error ?? undefined);
45
66
  });
46
67
  }
47
68
  catch (error) {
48
- reportFailure(error instanceof Error ? error : new Error(String(error)), resultCallback);
69
+ resolve(error instanceof Error ? error : new Error(String(error)));
70
+ }
71
+ });
72
+ const exportBatches = async (socket, endpoint, { droppedSpans, messages }) => {
73
+ const errors = await Promise.all(messages.map((message) => send(socket, message, endpoint)));
74
+ const error = errors.find((sendError) => sendError !== undefined) ??
75
+ (droppedSpans > 0
76
+ ? new Error(`${droppedSpans} span(s) exceed the UDP payload limit`)
77
+ : undefined);
78
+ if (error) {
79
+ api.diag.error('Error sending OTLP/UDP data: %s', error);
80
+ return { code: core.ExportResultCode.FAILED, error };
49
81
  }
82
+ return { code: core.ExportResultCode.SUCCESS };
50
83
  };
51
- const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT) => {
52
- const { host, port } = parseEndpoint(endpoint);
84
+ const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT, { maxPayloadBytes = MAX_UDP_PAYLOAD_BYTES } = {}) => {
85
+ const parsedEndpoint = parseEndpoint(endpoint);
53
86
  const socket = dgram.createSocket('udp4');
54
87
  socket.unref();
55
88
  return {
56
89
  export: (spans, resultCallback) => {
57
- const serialized = otlpTransformer.ProtobufTraceSerializer.serializeRequest(spans);
58
- if (serialized == null) {
59
- resultCallback({ code: core.ExportResultCode.SUCCESS });
60
- return;
61
- }
62
- const message = `${PROTOCOL_HEADER}${OTEL_TRACES_BINARY_PREFIX}${Buffer.from(serialized).toString('base64')}`;
63
- sendMessage(socket, message, { host, port }, resultCallback);
90
+ void exportBatches(socket, parsedEndpoint, encodeBatches(spans, Math.min(maxPayloadBytes, MAX_UDP_PAYLOAD_BYTES))).then(resultCallback);
64
91
  },
65
92
  forceFlush: () => Promise.resolve(),
66
93
  shutdown: () => new Promise((resolve) => {
@@ -101,22 +128,26 @@ const sdk = process.env.OTEL_ENABLED === 'true' ? createTracingSdk() : undefined
101
128
  const invocationParentContext = () => api.propagation.extract(api.context.active(), {
102
129
  'x-amzn-trace-id': process.env._X_AMZN_TRACE_ID ?? '',
103
130
  });
104
- const finishSpan = (span, error) => {
105
- if (error instanceof Error) {
106
- span.recordException(error);
107
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
108
- }
131
+ const failSpan = (span, error) => {
132
+ const exception = error instanceof Error ? error : new Error(String(error));
133
+ span.recordException(exception);
134
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: exception.message });
109
135
  span.end();
110
136
  };
111
137
  const logFlushFailure = () => {
112
138
  console.warn('OTel span flush failed');
113
139
  };
114
- const flushWithBudget = (flush) => Promise.race([
115
- Promise.resolve().then(flush).catch(logFlushFailure),
116
- new Promise((resolve) => {
117
- setTimeout(resolve, FLUSH_BUDGET_MS).unref();
118
- }),
119
- ]);
140
+ const flushWithBudget = (flush) => new Promise((resolve) => {
141
+ const budget = setTimeout(resolve, FLUSH_BUDGET_MS);
142
+ budget.unref();
143
+ void Promise.resolve()
144
+ .then(flush)
145
+ .catch(logFlushFailure)
146
+ .finally(() => {
147
+ clearTimeout(budget);
148
+ resolve();
149
+ });
150
+ });
120
151
  const getStatusCode = (result) => {
121
152
  if (typeof result !== 'object' || result === null) {
122
153
  return undefined;
@@ -139,11 +170,11 @@ const withInvocationSpan = async (attributes, run) => {
139
170
  if (statusCode !== undefined && statusCode >= 500) {
140
171
  span.setStatus({ code: api.SpanStatusCode.ERROR });
141
172
  }
142
- finishSpan(span);
173
+ span.end();
143
174
  return result;
144
175
  }
145
176
  catch (error) {
146
- finishSpan(span, error);
177
+ failSpan(span, error);
147
178
  throw error;
148
179
  }
149
180
  finally {
@@ -157,11 +188,11 @@ const withStageSpan = async (name, run) => {
157
188
  return sdk.tracer.startActiveSpan(name, async (span) => {
158
189
  try {
159
190
  const result = await run();
160
- finishSpan(span);
191
+ span.end();
161
192
  return result;
162
193
  }
163
194
  catch (error) {
164
- finishSpan(span, error);
195
+ failSpan(span, error);
165
196
  throw error;
166
197
  }
167
198
  });
@@ -16,6 +16,7 @@ const DEFAULT_ENDPOINT = '127.0.0.1:2000';
16
16
  const PROTOCOL_HEADER = '{"format":"json","version":1}\n';
17
17
  const OTEL_TRACES_BINARY_PREFIX = 'T1S';
18
18
  const MAX_PORT = 65_535;
19
+ const MAX_UDP_PAYLOAD_BYTES = 65_507;
19
20
  const parseEndpoint = (endpoint) => {
20
21
  const [host, port] = endpoint.split(':');
21
22
  const parsedPort = Number(port);
@@ -28,37 +29,63 @@ const parseEndpoint = (endpoint) => {
28
29
  }
29
30
  return { host, port: parsedPort };
30
31
  };
31
- const reportFailure = (error, resultCallback) => {
32
- diag.error('Error sending OTLP/UDP data: %s', error);
33
- resultCallback({ code: ExportResultCode.FAILED, error });
32
+ const encode = (spans) => {
33
+ const serialized = ProtobufTraceSerializer.serializeRequest(spans);
34
+ return serialized == null
35
+ ? undefined
36
+ : Buffer.from(`${PROTOCOL_HEADER}${OTEL_TRACES_BINARY_PREFIX}${Buffer.from(serialized).toString('base64')}`, 'utf8');
34
37
  };
35
- const sendMessage = (socket, message, { host, port }, resultCallback) => {
38
+ const encodeBatches = (spans, maxPayloadBytes) => {
39
+ if (spans.length === 0) {
40
+ return { droppedSpans: 0, messages: [] };
41
+ }
42
+ const message = encode(spans);
43
+ if (!message) {
44
+ return { droppedSpans: 0, messages: [] };
45
+ }
46
+ if (message.length <= maxPayloadBytes) {
47
+ return { droppedSpans: 0, messages: [message] };
48
+ }
49
+ if (spans.length === 1) {
50
+ return { droppedSpans: 1, messages: [] };
51
+ }
52
+ const middle = Math.ceil(spans.length / 2);
53
+ const head = encodeBatches(spans.slice(0, middle), maxPayloadBytes);
54
+ const tail = encodeBatches(spans.slice(middle), maxPayloadBytes);
55
+ return {
56
+ droppedSpans: head.droppedSpans + tail.droppedSpans,
57
+ messages: [...head.messages, ...tail.messages],
58
+ };
59
+ };
60
+ const send = (socket, message, { host, port }) => new Promise((resolve) => {
36
61
  try {
37
- socket.send(Buffer.from(message, 'utf8'), port, host, (error) => {
38
- if (error) {
39
- reportFailure(error, resultCallback);
40
- return;
41
- }
42
- resultCallback({ code: ExportResultCode.SUCCESS });
62
+ socket.send(message, port, host, (error) => {
63
+ resolve(error ?? undefined);
43
64
  });
44
65
  }
45
66
  catch (error) {
46
- reportFailure(error instanceof Error ? error : new Error(String(error)), resultCallback);
67
+ resolve(error instanceof Error ? error : new Error(String(error)));
68
+ }
69
+ });
70
+ const exportBatches = async (socket, endpoint, { droppedSpans, messages }) => {
71
+ const errors = await Promise.all(messages.map((message) => send(socket, message, endpoint)));
72
+ const error = errors.find((sendError) => sendError !== undefined) ??
73
+ (droppedSpans > 0
74
+ ? new Error(`${droppedSpans} span(s) exceed the UDP payload limit`)
75
+ : undefined);
76
+ if (error) {
77
+ diag.error('Error sending OTLP/UDP data: %s', error);
78
+ return { code: ExportResultCode.FAILED, error };
47
79
  }
80
+ return { code: ExportResultCode.SUCCESS };
48
81
  };
49
- const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT) => {
50
- const { host, port } = parseEndpoint(endpoint);
82
+ const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT, { maxPayloadBytes = MAX_UDP_PAYLOAD_BYTES } = {}) => {
83
+ const parsedEndpoint = parseEndpoint(endpoint);
51
84
  const socket = dgram.createSocket('udp4');
52
85
  socket.unref();
53
86
  return {
54
87
  export: (spans, resultCallback) => {
55
- const serialized = ProtobufTraceSerializer.serializeRequest(spans);
56
- if (serialized == null) {
57
- resultCallback({ code: ExportResultCode.SUCCESS });
58
- return;
59
- }
60
- const message = `${PROTOCOL_HEADER}${OTEL_TRACES_BINARY_PREFIX}${Buffer.from(serialized).toString('base64')}`;
61
- sendMessage(socket, message, { host, port }, resultCallback);
88
+ void exportBatches(socket, parsedEndpoint, encodeBatches(spans, Math.min(maxPayloadBytes, MAX_UDP_PAYLOAD_BYTES))).then(resultCallback);
62
89
  },
63
90
  forceFlush: () => Promise.resolve(),
64
91
  shutdown: () => new Promise((resolve) => {
@@ -99,22 +126,26 @@ const sdk = process.env.OTEL_ENABLED === 'true' ? createTracingSdk() : undefined
99
126
  const invocationParentContext = () => propagation.extract(context.active(), {
100
127
  'x-amzn-trace-id': process.env._X_AMZN_TRACE_ID ?? '',
101
128
  });
102
- const finishSpan = (span, error) => {
103
- if (error instanceof Error) {
104
- span.recordException(error);
105
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
106
- }
129
+ const failSpan = (span, error) => {
130
+ const exception = error instanceof Error ? error : new Error(String(error));
131
+ span.recordException(exception);
132
+ span.setStatus({ code: SpanStatusCode.ERROR, message: exception.message });
107
133
  span.end();
108
134
  };
109
135
  const logFlushFailure = () => {
110
136
  console.warn('OTel span flush failed');
111
137
  };
112
- const flushWithBudget = (flush) => Promise.race([
113
- Promise.resolve().then(flush).catch(logFlushFailure),
114
- new Promise((resolve) => {
115
- setTimeout(resolve, FLUSH_BUDGET_MS).unref();
116
- }),
117
- ]);
138
+ const flushWithBudget = (flush) => new Promise((resolve) => {
139
+ const budget = setTimeout(resolve, FLUSH_BUDGET_MS);
140
+ budget.unref();
141
+ void Promise.resolve()
142
+ .then(flush)
143
+ .catch(logFlushFailure)
144
+ .finally(() => {
145
+ clearTimeout(budget);
146
+ resolve();
147
+ });
148
+ });
118
149
  const getStatusCode = (result) => {
119
150
  if (typeof result !== 'object' || result === null) {
120
151
  return undefined;
@@ -137,11 +168,11 @@ const withInvocationSpan = async (attributes, run) => {
137
168
  if (statusCode !== undefined && statusCode >= 500) {
138
169
  span.setStatus({ code: SpanStatusCode.ERROR });
139
170
  }
140
- finishSpan(span);
171
+ span.end();
141
172
  return result;
142
173
  }
143
174
  catch (error) {
144
- finishSpan(span, error);
175
+ failSpan(span, error);
145
176
  throw error;
146
177
  }
147
178
  finally {
@@ -155,11 +186,11 @@ const withStageSpan = async (name, run) => {
155
186
  return sdk.tracer.startActiveSpan(name, async (span) => {
156
187
  try {
157
188
  const result = await run();
158
- finishSpan(span);
189
+ span.end();
159
190
  return result;
160
191
  }
161
192
  catch (error) {
162
- finishSpan(span, error);
193
+ failSpan(span, error);
163
194
  throw error;
164
195
  }
165
196
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allthings/cloud-toolkit",
3
- "version": "5.4.0-beta.1",
3
+ "version": "5.4.0",
4
4
  "description": "",
5
5
  "author": "Allthings GmbH",
6
6
  "keywords": [