@lunora/container 1.0.0-alpha.14 → 1.0.0-alpha.15

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/dist/otel.d.mts CHANGED
@@ -61,6 +61,20 @@ interface ContainerLogInput {
61
61
  * @experimental
62
62
  */
63
63
  interface ContainerTelemetryOptions {
64
+ /**
65
+ * Value of the `deployment.environment` resource attribute. Falls back to
66
+ * the `DEPLOYMENT_ENVIRONMENT` / `ENVIRONMENT` / `NODE_ENV` env vars **only
67
+ * when {@link ContainerTelemetryOptions.detectResources} is `true`** —
68
+ * unlike `serviceName`, env detection here is opt-in so a stray `NODE_ENV`
69
+ * never silently labels a deployment.
70
+ */
71
+ deploymentEnvironment?: string;
72
+ /**
73
+ * When `true`, auto-detect OTLP resource attributes from the container
74
+ * environment (`HOSTNAME`, `KUBERNETES_*`, `SERVICE_VERSION`, etc.).
75
+ * Explicit options and `resourceAttributes` win on collision.
76
+ */
77
+ detectResources?: boolean;
64
78
  /** Base OTLP collector endpoint; defaults to the `LUNORA_OTLP_ENDPOINT` env var. */
65
79
  endpoint?: string;
66
80
  /** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
@@ -69,8 +83,17 @@ interface ContainerTelemetryOptions {
69
83
  headers?: Record<string, string>;
70
84
  /** Called with any send failure so the caller can surface it; the export itself always swallows. */
71
85
  onError?: (error: unknown) => void;
86
+ /** Additional resource attributes merged onto every signal. */
87
+ resourceAttributes?: Record<string, ContainerAttributeValue>;
72
88
  /** `service.name` resource attribute; defaults to the `LUNORA_SERVICE_NAME` env var then `"lunora-container"`. */
73
89
  serviceName?: string;
90
+ /**
91
+ * `service.version` resource attribute. Falls back to `SERVICE_VERSION` /
92
+ * `CF_VERSION_METADATA` / `VERCEL_GIT_COMMIT_SHA` / `GITHUB_SHA` /
93
+ * `COMMIT_SHA` env vars **only when
94
+ * {@link ContainerTelemetryOptions.detectResources} is `true`**.
95
+ */
96
+ serviceVersion?: string;
74
97
  /** Per-POST timeout in ms; a collector that never responds aborts after this so a stuck send can't stall `flush()`. Defaults to {@link DEFAULT_TIMEOUT_MS} (10s). */
75
98
  timeoutMs?: number;
76
99
  /** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
@@ -128,7 +151,10 @@ interface ContainerTelemetry {
128
151
  * With no endpoint resolvable the returned exporter is disabled (`enabled ===
129
152
  * false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
130
153
  * nothing — so the same code runs unchanged locally and in the cloud.
131
- * @param options Exporter options; every field falls back to a `LUNORA_*` env var.
154
+ * @param options Exporter options. Connection fields (`endpoint`, `token`,
155
+ * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
156
+ * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
157
+ * `detectResources: true`.
132
158
  * @experimental
133
159
  */
134
160
  declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
package/dist/otel.d.ts CHANGED
@@ -61,6 +61,20 @@ interface ContainerLogInput {
61
61
  * @experimental
62
62
  */
63
63
  interface ContainerTelemetryOptions {
64
+ /**
65
+ * Value of the `deployment.environment` resource attribute. Falls back to
66
+ * the `DEPLOYMENT_ENVIRONMENT` / `ENVIRONMENT` / `NODE_ENV` env vars **only
67
+ * when {@link ContainerTelemetryOptions.detectResources} is `true`** —
68
+ * unlike `serviceName`, env detection here is opt-in so a stray `NODE_ENV`
69
+ * never silently labels a deployment.
70
+ */
71
+ deploymentEnvironment?: string;
72
+ /**
73
+ * When `true`, auto-detect OTLP resource attributes from the container
74
+ * environment (`HOSTNAME`, `KUBERNETES_*`, `SERVICE_VERSION`, etc.).
75
+ * Explicit options and `resourceAttributes` win on collision.
76
+ */
77
+ detectResources?: boolean;
64
78
  /** Base OTLP collector endpoint; defaults to the `LUNORA_OTLP_ENDPOINT` env var. */
65
79
  endpoint?: string;
66
80
  /** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
@@ -69,8 +83,17 @@ interface ContainerTelemetryOptions {
69
83
  headers?: Record<string, string>;
70
84
  /** Called with any send failure so the caller can surface it; the export itself always swallows. */
71
85
  onError?: (error: unknown) => void;
86
+ /** Additional resource attributes merged onto every signal. */
87
+ resourceAttributes?: Record<string, ContainerAttributeValue>;
72
88
  /** `service.name` resource attribute; defaults to the `LUNORA_SERVICE_NAME` env var then `"lunora-container"`. */
73
89
  serviceName?: string;
90
+ /**
91
+ * `service.version` resource attribute. Falls back to `SERVICE_VERSION` /
92
+ * `CF_VERSION_METADATA` / `VERCEL_GIT_COMMIT_SHA` / `GITHUB_SHA` /
93
+ * `COMMIT_SHA` env vars **only when
94
+ * {@link ContainerTelemetryOptions.detectResources} is `true`**.
95
+ */
96
+ serviceVersion?: string;
74
97
  /** Per-POST timeout in ms; a collector that never responds aborts after this so a stuck send can't stall `flush()`. Defaults to {@link DEFAULT_TIMEOUT_MS} (10s). */
75
98
  timeoutMs?: number;
76
99
  /** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
@@ -128,7 +151,10 @@ interface ContainerTelemetry {
128
151
  * With no endpoint resolvable the returned exporter is disabled (`enabled ===
129
152
  * false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
130
153
  * nothing — so the same code runs unchanged locally and in the cloud.
131
- * @param options Exporter options; every field falls back to a `LUNORA_*` env var.
154
+ * @param options Exporter options. Connection fields (`endpoint`, `token`,
155
+ * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
156
+ * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
157
+ * `detectResources: true`.
132
158
  * @experimental
133
159
  */
134
160
  declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
package/dist/otel.mjs CHANGED
@@ -35,7 +35,7 @@ const parseTraceparent = (header) => {
35
35
  version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
36
36
  return void 0;
37
37
  }
38
- return { parentSpanId, traceId };
38
+ return { parentSpanId, sampled: (Number.parseInt(flags, 16) & 1) === 1, traceId };
39
39
  };
40
40
  const encodeAttribute = (key, value) => {
41
41
  if (typeof value === "boolean") {
@@ -79,31 +79,82 @@ const mergeHeaders = (defaults, overrides, token) => {
79
79
  }
80
80
  return merged;
81
81
  };
82
- const wrapResourceSpans = (span, scopeName, serviceName) => {
82
+ const buildResourceAttributes = (serviceName, extra) => {
83
+ const merged = { "service.name": serviceName };
84
+ for (const [key, value] of Object.entries(extra ?? {})) {
85
+ merged[key] = value;
86
+ }
87
+ return Object.entries(merged).map(([key, value]) => encodeAttribute(key, value));
88
+ };
89
+ const wrapResourceSpans = (span, scopeName, serviceName, resourceAttributes) => {
83
90
  return {
84
91
  resourceSpans: [
85
92
  {
86
- resource: { attributes: [encodeAttribute("service.name", serviceName)] },
93
+ resource: { attributes: buildResourceAttributes(serviceName, resourceAttributes) },
87
94
  scopeSpans: [{ scope: { name: scopeName }, spans: [span] }]
88
95
  }
89
96
  ]
90
97
  };
91
98
  };
92
- const wrapResourceLogs = (logRecord, scopeName, serviceName) => {
99
+ const wrapResourceLogs = (logRecord, scopeName, serviceName, resourceAttributes) => {
93
100
  return {
94
101
  resourceLogs: [
95
102
  {
96
- resource: { attributes: [encodeAttribute("service.name", serviceName)] },
103
+ resource: { attributes: buildResourceAttributes(serviceName, resourceAttributes) },
97
104
  scopeLogs: [{ logRecords: [logRecord], scope: { name: scopeName } }]
98
105
  }
99
106
  ]
100
107
  };
101
108
  };
102
109
 
110
+ const detectServiceResource = (read) => {
111
+ const detected = {};
112
+ const serviceVersion = read("SERVICE_VERSION") ?? read("CF_VERSION_METADATA") ?? read("VERCEL_GIT_COMMIT_SHA") ?? read("GITHUB_SHA") ?? read("COMMIT_SHA");
113
+ if (serviceVersion !== void 0) {
114
+ detected["service.version"] = serviceVersion;
115
+ }
116
+ const deploymentEnvironment = read("DEPLOYMENT_ENVIRONMENT") ?? read("ENVIRONMENT") ?? read("NODE_ENV");
117
+ if (deploymentEnvironment !== void 0) {
118
+ detected["deployment.environment"] = deploymentEnvironment;
119
+ }
120
+ return detected;
121
+ };
122
+ const detectHostResource = (read, pid) => {
123
+ const detected = {};
124
+ const hostName = read("HOSTNAME") ?? read("COMPUTERNAME");
125
+ if (hostName !== void 0) {
126
+ detected["host.name"] = hostName;
127
+ }
128
+ const podName = read("KUBERNETES_POD_NAME") ?? read("HOSTNAME");
129
+ if (podName !== void 0 && read("KUBERNETES_SERVICE_HOST") !== void 0) {
130
+ detected["k8s.pod.name"] = podName;
131
+ }
132
+ if (pid !== void 0 && Number.isFinite(pid)) {
133
+ detected["process.pid"] = pid;
134
+ }
135
+ return detected;
136
+ };
137
+ const mergeResourceAttributes = (...bags) => {
138
+ const merged = {};
139
+ for (const bag of bags) {
140
+ if (bag === void 0) {
141
+ continue;
142
+ }
143
+ for (const [key, value] of Object.entries(bag)) {
144
+ merged[key] = value;
145
+ }
146
+ }
147
+ return merged;
148
+ };
149
+
103
150
  const DEFAULT_TIMEOUT_MS = 1e4;
104
151
  const readEnv = (name) => {
105
152
  return process.env[name];
106
153
  };
154
+ const detectContainerResource = () => {
155
+ const pid = typeof process.pid !== "number" ? void 0 : process.pid;
156
+ return mergeResourceAttributes(detectServiceResource(readEnv), detectHostResource(readEnv, pid));
157
+ };
107
158
  const resolveFetch = (injected) => {
108
159
  if (injected !== void 0) {
109
160
  return injected;
@@ -113,7 +164,7 @@ const resolveFetch = (injected) => {
113
164
  }
114
165
  return void 0;
115
166
  };
116
- const traceBody = (span, serviceName, parent) => {
167
+ const traceBody = (span, serviceName, parent, resource) => {
117
168
  const attributes = encodeAttributes(span.attributes);
118
169
  if (span.error?.type !== void 0) {
119
170
  attributes.push(encodeAttribute("error.type", span.error.type));
@@ -133,9 +184,18 @@ const traceBody = (span, serviceName, parent) => {
133
184
  status: span.error === void 0 ? { code: 1 } : { code: 2, message: span.error.message },
134
185
  traceId: parent?.traceId ?? otlpRandomHex(16)
135
186
  };
136
- return wrapResourceSpans(otlpSpan, "@lunora/container", serviceName);
187
+ if (span.error) {
188
+ otlpSpan.events = [
189
+ {
190
+ attributes: [encodeAttribute("exception.type", span.error.type ?? "Error"), encodeAttribute("exception.message", span.error.message)],
191
+ name: "exception",
192
+ timeUnixNano: otlpUnixNano(span.endMs)
193
+ }
194
+ ];
195
+ }
196
+ return wrapResourceSpans(otlpSpan, "@lunora/container", serviceName, resource);
137
197
  };
138
- const logBody = (log, serviceName, nowMs) => {
198
+ const logBody = (log, serviceName, nowMs, resource) => {
139
199
  const level = log.level ?? "info";
140
200
  const record = {
141
201
  attributes: encodeAttributes(log.attributes),
@@ -144,7 +204,7 @@ const logBody = (log, serviceName, nowMs) => {
144
204
  severityText: level.toUpperCase(),
145
205
  timeUnixNano: otlpUnixNano(log.ts ?? nowMs)
146
206
  };
147
- return wrapResourceLogs(record, "@lunora/container", serviceName);
207
+ return wrapResourceLogs(record, "@lunora/container", serviceName, resource);
148
208
  };
149
209
  const createContainerTelemetry = (options = {}) => {
150
210
  const endpoint = options.endpoint ?? readEnv("LUNORA_OTLP_ENDPOINT");
@@ -155,6 +215,18 @@ const createContainerTelemetry = (options = {}) => {
155
215
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
156
216
  const fetchImpl = resolveFetch(options.fetch);
157
217
  const headers = mergeHeaders({ "content-type": "application/json" }, options.headers, token);
218
+ const detectedAttributes = options.detectResources === true ? detectContainerResource() : void 0;
219
+ const staticAttributes = {};
220
+ if (options.serviceVersion !== void 0) {
221
+ staticAttributes["service.version"] = options.serviceVersion;
222
+ }
223
+ if (options.deploymentEnvironment !== void 0) {
224
+ staticAttributes["deployment.environment"] = options.deploymentEnvironment;
225
+ }
226
+ if (options.resourceAttributes !== void 0) {
227
+ Object.assign(staticAttributes, options.resourceAttributes);
228
+ }
229
+ const resource = mergeResourceAttributes(detectedAttributes, staticAttributes);
158
230
  let base = endpoint ?? "";
159
231
  while (base.endsWith("/")) {
160
232
  base = base.slice(0, -1);
@@ -190,13 +262,13 @@ const createContainerTelemetry = (options = {}) => {
190
262
  if (!enabled) {
191
263
  return;
192
264
  }
193
- send(tracesUrl, traceBody(span, serviceName, parent));
265
+ send(tracesUrl, traceBody(span, serviceName, parent, resource));
194
266
  };
195
267
  const emitLog = (log) => {
196
268
  if (!enabled) {
197
269
  return;
198
270
  }
199
- send(logsUrl, logBody(log, serviceName, Date.now()));
271
+ send(logsUrl, logBody(log, serviceName, Date.now(), resource));
200
272
  };
201
273
  const trace = async (name, run, attributes) => {
202
274
  const startMs = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/container",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "Cloudflare Containers for Lunora: defineContainer, generated Container DO classes, and the ctx.containers action surface",
5
5
  "keywords": [
6
6
  "cloudflare",