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

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,60 @@ 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
+ const message = encode(spans);
42
+ if (!message) {
43
+ return { droppedSpans: 0, messages: [] };
44
+ }
45
+ if (message.length <= maxPayloadBytes) {
46
+ return { droppedSpans: 0, messages: [message] };
47
+ }
48
+ if (spans.length === 1) {
49
+ return { droppedSpans: 1, messages: [] };
50
+ }
51
+ const middle = Math.ceil(spans.length / 2);
52
+ const head = encodeBatches(spans.slice(0, middle), maxPayloadBytes);
53
+ const tail = encodeBatches(spans.slice(middle), maxPayloadBytes);
54
+ return {
55
+ droppedSpans: head.droppedSpans + tail.droppedSpans,
56
+ messages: [...head.messages, ...tail.messages],
57
+ };
58
+ };
59
+ const send = (socket, message, { host, port }) => new Promise((resolve) => {
38
60
  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 });
61
+ socket.send(message, port, host, (error) => {
62
+ resolve(error ?? undefined);
45
63
  });
46
64
  }
47
65
  catch (error) {
48
- reportFailure(error instanceof Error ? error : new Error(String(error)), resultCallback);
66
+ resolve(error instanceof Error ? error : new Error(String(error)));
49
67
  }
68
+ });
69
+ const exportBatches = async (socket, endpoint, { droppedSpans, messages }) => {
70
+ const errors = await Promise.all(messages.map((message) => send(socket, message, endpoint)));
71
+ const error = errors.find((sendError) => sendError !== undefined) ??
72
+ (droppedSpans > 0
73
+ ? new Error(`${droppedSpans} span(s) exceed the UDP payload limit`)
74
+ : undefined);
75
+ if (error) {
76
+ api.diag.error('Error sending OTLP/UDP data: %s', error);
77
+ return { code: core.ExportResultCode.FAILED, error };
78
+ }
79
+ return { code: core.ExportResultCode.SUCCESS };
50
80
  };
51
- const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT) => {
52
- const { host, port } = parseEndpoint(endpoint);
81
+ const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT, { maxPayloadBytes = MAX_UDP_PAYLOAD_BYTES } = {}) => {
82
+ const parsedEndpoint = parseEndpoint(endpoint);
53
83
  const socket = dgram.createSocket('udp4');
54
84
  socket.unref();
55
85
  return {
56
86
  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);
87
+ void exportBatches(socket, parsedEndpoint, encodeBatches(spans, maxPayloadBytes)).then(resultCallback);
64
88
  },
65
89
  forceFlush: () => Promise.resolve(),
66
90
  shutdown: () => new Promise((resolve) => {
@@ -101,22 +125,26 @@ const sdk = process.env.OTEL_ENABLED === 'true' ? createTracingSdk() : undefined
101
125
  const invocationParentContext = () => api.propagation.extract(api.context.active(), {
102
126
  'x-amzn-trace-id': process.env._X_AMZN_TRACE_ID ?? '',
103
127
  });
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
- }
128
+ const failSpan = (span, error) => {
129
+ const exception = error instanceof Error ? error : new Error(String(error));
130
+ span.recordException(exception);
131
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: exception.message });
109
132
  span.end();
110
133
  };
111
134
  const logFlushFailure = () => {
112
135
  console.warn('OTel span flush failed');
113
136
  };
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
- ]);
137
+ const flushWithBudget = (flush) => new Promise((resolve) => {
138
+ const budget = setTimeout(resolve, FLUSH_BUDGET_MS);
139
+ budget.unref();
140
+ void Promise.resolve()
141
+ .then(flush)
142
+ .catch(logFlushFailure)
143
+ .finally(() => {
144
+ clearTimeout(budget);
145
+ resolve();
146
+ });
147
+ });
120
148
  const getStatusCode = (result) => {
121
149
  if (typeof result !== 'object' || result === null) {
122
150
  return undefined;
@@ -139,11 +167,11 @@ const withInvocationSpan = async (attributes, run) => {
139
167
  if (statusCode !== undefined && statusCode >= 500) {
140
168
  span.setStatus({ code: api.SpanStatusCode.ERROR });
141
169
  }
142
- finishSpan(span);
170
+ span.end();
143
171
  return result;
144
172
  }
145
173
  catch (error) {
146
- finishSpan(span, error);
174
+ failSpan(span, error);
147
175
  throw error;
148
176
  }
149
177
  finally {
@@ -157,11 +185,11 @@ const withStageSpan = async (name, run) => {
157
185
  return sdk.tracer.startActiveSpan(name, async (span) => {
158
186
  try {
159
187
  const result = await run();
160
- finishSpan(span);
188
+ span.end();
161
189
  return result;
162
190
  }
163
191
  catch (error) {
164
- finishSpan(span, error);
192
+ failSpan(span, error);
165
193
  throw error;
166
194
  }
167
195
  });
@@ -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,60 @@ 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
+ const message = encode(spans);
40
+ if (!message) {
41
+ return { droppedSpans: 0, messages: [] };
42
+ }
43
+ if (message.length <= maxPayloadBytes) {
44
+ return { droppedSpans: 0, messages: [message] };
45
+ }
46
+ if (spans.length === 1) {
47
+ return { droppedSpans: 1, messages: [] };
48
+ }
49
+ const middle = Math.ceil(spans.length / 2);
50
+ const head = encodeBatches(spans.slice(0, middle), maxPayloadBytes);
51
+ const tail = encodeBatches(spans.slice(middle), maxPayloadBytes);
52
+ return {
53
+ droppedSpans: head.droppedSpans + tail.droppedSpans,
54
+ messages: [...head.messages, ...tail.messages],
55
+ };
56
+ };
57
+ const send = (socket, message, { host, port }) => new Promise((resolve) => {
36
58
  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 });
59
+ socket.send(message, port, host, (error) => {
60
+ resolve(error ?? undefined);
43
61
  });
44
62
  }
45
63
  catch (error) {
46
- reportFailure(error instanceof Error ? error : new Error(String(error)), resultCallback);
64
+ resolve(error instanceof Error ? error : new Error(String(error)));
47
65
  }
66
+ });
67
+ const exportBatches = async (socket, endpoint, { droppedSpans, messages }) => {
68
+ const errors = await Promise.all(messages.map((message) => send(socket, message, endpoint)));
69
+ const error = errors.find((sendError) => sendError !== undefined) ??
70
+ (droppedSpans > 0
71
+ ? new Error(`${droppedSpans} span(s) exceed the UDP payload limit`)
72
+ : undefined);
73
+ if (error) {
74
+ diag.error('Error sending OTLP/UDP data: %s', error);
75
+ return { code: ExportResultCode.FAILED, error };
76
+ }
77
+ return { code: ExportResultCode.SUCCESS };
48
78
  };
49
- const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT) => {
50
- const { host, port } = parseEndpoint(endpoint);
79
+ const createOtlpUdpSpanExporter = (endpoint = DEFAULT_ENDPOINT, { maxPayloadBytes = MAX_UDP_PAYLOAD_BYTES } = {}) => {
80
+ const parsedEndpoint = parseEndpoint(endpoint);
51
81
  const socket = dgram.createSocket('udp4');
52
82
  socket.unref();
53
83
  return {
54
84
  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);
85
+ void exportBatches(socket, parsedEndpoint, encodeBatches(spans, maxPayloadBytes)).then(resultCallback);
62
86
  },
63
87
  forceFlush: () => Promise.resolve(),
64
88
  shutdown: () => new Promise((resolve) => {
@@ -99,22 +123,26 @@ const sdk = process.env.OTEL_ENABLED === 'true' ? createTracingSdk() : undefined
99
123
  const invocationParentContext = () => propagation.extract(context.active(), {
100
124
  'x-amzn-trace-id': process.env._X_AMZN_TRACE_ID ?? '',
101
125
  });
102
- const finishSpan = (span, error) => {
103
- if (error instanceof Error) {
104
- span.recordException(error);
105
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
106
- }
126
+ const failSpan = (span, error) => {
127
+ const exception = error instanceof Error ? error : new Error(String(error));
128
+ span.recordException(exception);
129
+ span.setStatus({ code: SpanStatusCode.ERROR, message: exception.message });
107
130
  span.end();
108
131
  };
109
132
  const logFlushFailure = () => {
110
133
  console.warn('OTel span flush failed');
111
134
  };
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
- ]);
135
+ const flushWithBudget = (flush) => new Promise((resolve) => {
136
+ const budget = setTimeout(resolve, FLUSH_BUDGET_MS);
137
+ budget.unref();
138
+ void Promise.resolve()
139
+ .then(flush)
140
+ .catch(logFlushFailure)
141
+ .finally(() => {
142
+ clearTimeout(budget);
143
+ resolve();
144
+ });
145
+ });
118
146
  const getStatusCode = (result) => {
119
147
  if (typeof result !== 'object' || result === null) {
120
148
  return undefined;
@@ -137,11 +165,11 @@ const withInvocationSpan = async (attributes, run) => {
137
165
  if (statusCode !== undefined && statusCode >= 500) {
138
166
  span.setStatus({ code: SpanStatusCode.ERROR });
139
167
  }
140
- finishSpan(span);
168
+ span.end();
141
169
  return result;
142
170
  }
143
171
  catch (error) {
144
- finishSpan(span, error);
172
+ failSpan(span, error);
145
173
  throw error;
146
174
  }
147
175
  finally {
@@ -155,11 +183,11 @@ const withStageSpan = async (name, run) => {
155
183
  return sdk.tracer.startActiveSpan(name, async (span) => {
156
184
  try {
157
185
  const result = await run();
158
- finishSpan(span);
186
+ span.end();
159
187
  return result;
160
188
  }
161
189
  catch (error) {
162
- finishSpan(span, error);
190
+ failSpan(span, error);
163
191
  throw error;
164
192
  }
165
193
  });
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-beta.2",
4
4
  "description": "",
5
5
  "author": "Allthings GmbH",
6
6
  "keywords": [