autotel 7.3.0 → 7.5.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.
Files changed (56) hide show
  1. package/README.md +17 -0
  2. package/dist/auto.cjs +1 -1
  3. package/dist/auto.js +1 -1
  4. package/dist/{canonical-log-line-processor-DolzdawH.cjs → canonical-log-line-processor-B-tzezhk.cjs} +24 -10
  5. package/dist/{canonical-log-line-processor-l0Aqe6yp.d.cts → canonical-log-line-processor-C0Z_N4oL.d.cts} +17 -3
  6. package/dist/{canonical-log-line-processor-hFUmS__K.js → canonical-log-line-processor-CBv-OCp4.js} +24 -10
  7. package/dist/{canonical-log-line-processor-cPB4Axk-.d.ts → canonical-log-line-processor-Yq4cXcD5.d.ts} +17 -3
  8. package/dist/correlation-id.cjs +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/event.cjs +1 -1
  13. package/dist/event.js +1 -1
  14. package/dist/feature-flags.cjs +204 -0
  15. package/dist/feature-flags.d.cts +175 -0
  16. package/dist/feature-flags.d.ts +175 -0
  17. package/dist/feature-flags.js +198 -0
  18. package/dist/{functional-C4js2vTM.js → functional-B86zszmZ.js} +1 -1
  19. package/dist/{functional-DHlD7V0x.cjs → functional-BOZ3me6w.cjs} +1 -1
  20. package/dist/functional.cjs +1 -1
  21. package/dist/functional.js +1 -1
  22. package/dist/http.cjs +1 -1
  23. package/dist/http.js +1 -1
  24. package/dist/index.cjs +3 -3
  25. package/dist/index.d.cts +2 -2
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +3 -3
  28. package/dist/{init-CsKK9vZO.js → init-B2kv4bLO.js} +31 -12
  29. package/dist/{init-8GSy2bza.d.cts → init-BU7qa-Ii.d.cts} +67 -6
  30. package/dist/{init-DmM7dprX.cjs → init-CNUkdwke.cjs} +31 -12
  31. package/dist/{init-UnKrXhpq.d.ts → init-DDCgmfa4.d.ts} +67 -6
  32. package/dist/instrumentation.cjs +1 -1
  33. package/dist/instrumentation.js +1 -1
  34. package/dist/messaging.cjs +2 -2
  35. package/dist/messaging.js +2 -2
  36. package/dist/processors.cjs +1 -1
  37. package/dist/processors.d.cts +1 -1
  38. package/dist/processors.d.ts +1 -1
  39. package/dist/processors.js +1 -1
  40. package/dist/sampling.cjs +1 -1
  41. package/dist/sampling.js +1 -1
  42. package/dist/semantic-helpers.cjs +1 -1
  43. package/dist/semantic-helpers.js +1 -1
  44. package/dist/tail-sampling-processor.cjs +1 -1
  45. package/dist/tail-sampling-processor.js +1 -1
  46. package/dist/webhook.cjs +1 -1
  47. package/dist/webhook.js +1 -1
  48. package/dist/workflow-distributed.cjs +1 -1
  49. package/dist/workflow-distributed.js +1 -1
  50. package/dist/workflow.cjs +1 -1
  51. package/dist/workflow.js +1 -1
  52. package/dist/yaml-config.cjs +1 -1
  53. package/dist/yaml-config.d.cts +1 -1
  54. package/dist/yaml-config.d.ts +1 -1
  55. package/dist/yaml-config.js +1 -1
  56. package/package.json +6 -1
package/README.md CHANGED
@@ -1836,6 +1836,23 @@ WHERE duration_ms > 200
1836
1836
 
1837
1837
  ### Configuration Options
1838
1838
 
1839
+ To keep canonical lines in a platform logger and send the same records to
1840
+ Grafana Loki over OTLP, enable both destinations. Selecting `otel: true`
1841
+ auto-configures log export from `endpoint`; an explicit `logs: false` or
1842
+ `AUTOTEL_LOGS=off` still wins.
1843
+
1844
+ ```typescript
1845
+ init({
1846
+ service: 'my-app',
1847
+ endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
1848
+ canonicalLogLines: {
1849
+ enabled: true,
1850
+ logger: [pino(), auditLogger],
1851
+ otel: true,
1852
+ },
1853
+ });
1854
+ ```
1855
+
1839
1856
  ```typescript
1840
1857
  init({
1841
1858
  service: 'my-app',
package/dist/auto.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
2
- const require_init = require('./init-DmM7dprX.cjs');
2
+ const require_init = require('./init-CNUkdwke.cjs');
3
3
  let node_module = require("node:module");
4
4
  node_module = require_rolldown_runtime.__toESM(node_module, 1);
5
5
  let import_in_the_middle = require("import-in-the-middle");
package/dist/auto.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as loadYamlConfig, a as init } from "./init-CsKK9vZO.js";
1
+ import { _ as loadYamlConfig, a as init } from "./init-B2kv4bLO.js";
2
2
  import * as nodeModule from "node:module";
3
3
  import { createAddHookMessageChannel } from "import-in-the-middle";
4
4
 
@@ -233,7 +233,8 @@ const SEVERITY_NUMBERS = /* @__PURE__ */ new Map([
233
233
  * ```
234
234
  */
235
235
  var CanonicalLogLineProcessor = class {
236
- logger;
236
+ loggers;
237
+ useOtel;
237
238
  rootSpansOnly;
238
239
  minLevel;
239
240
  messageFormat;
@@ -245,7 +246,8 @@ var CanonicalLogLineProcessor = class {
245
246
  pretty;
246
247
  getOTelLogger = null;
247
248
  constructor(options = {}) {
248
- this.logger = options.logger;
249
+ this.loggers = options.logger ? Array.isArray(options.logger) ? options.logger.filter(Boolean) : [options.logger] : [];
250
+ this.useOtel = options.otel ?? this.loggers.length === 0;
249
251
  this.rootSpansOnly = options.rootSpansOnly ?? false;
250
252
  this.minLevel = options.minLevel ?? "info";
251
253
  this.messageFormat = options.messageFormat ?? ((span) => `[${span.name}] Request completed`);
@@ -255,7 +257,7 @@ var CanonicalLogLineProcessor = class {
255
257
  this.drain = options.drain;
256
258
  this.onDrainError = options.onDrainError;
257
259
  this.pretty = options.pretty ?? (require_values.hasProcess() && process.env.NODE_ENV === "development");
258
- if (!this.logger) this.getOTelLogger = () => _opentelemetry_api_logs.logs.getLogger("autotel.canonical-log-line");
260
+ if (this.useOtel) this.getOTelLogger = () => _opentelemetry_api_logs.logs.getLogger("autotel.canonical-log-line");
259
261
  }
260
262
  buildKeepPredicate(keep) {
261
263
  if (!keep || keep.length === 0) return void 0;
@@ -287,10 +289,12 @@ var CanonicalLogLineProcessor = class {
287
289
  };
288
290
  if (this.shouldEmit && !this.shouldEmit(eventContext)) return;
289
291
  if (this.pretty) console.log(formatPrettyLogLine(eventContext));
290
- if (this.logger) this.emitViaLogger(level, message, canonicalLogLine);
291
- else if (this.getOTelLogger) {
292
+ if (this.loggers.length > 0) this.emitViaLogger(level, message, canonicalLogLine);
293
+ if (this.getOTelLogger) try {
292
294
  const otelLogger = this.getOTelLogger();
293
295
  this.emitViaOTel(level, message, canonicalLogLine, otelLogger);
296
+ } catch (error) {
297
+ this.reportInternalWarning("canonical log line OTel emission failed", error);
294
298
  }
295
299
  if (this.drain) Promise.resolve(this.drain(eventContext)).catch((error) => {
296
300
  if (this.onDrainError) {
@@ -328,7 +332,11 @@ var CanonicalLogLineProcessor = class {
328
332
  return redacted;
329
333
  }
330
334
  emitViaLogger(level, message, canonicalLogLine) {
331
- this.logger[level](canonicalLogLine, message);
335
+ for (const logger of this.loggers) try {
336
+ logger[level](canonicalLogLine, message);
337
+ } catch (error) {
338
+ this.reportInternalWarning("canonical log line logger emission failed", error);
339
+ }
332
340
  }
333
341
  emitViaOTel(level, message, canonicalLogLine, otelLogger) {
334
342
  const otelAttributes = {};
@@ -364,11 +372,17 @@ var CanonicalLogLineProcessor = class {
364
372
  }
365
373
  reportInternalWarning(message, cause) {
366
374
  const err = cause === void 0 ? "unknown error" : require_values.toError(cause).message;
367
- if (this.logger) {
368
- this.logger.warn({ error: err }, `[autotel] ${message}`);
369
- return;
375
+ if (this.loggers.length > 0) {
376
+ let reported = false;
377
+ for (const logger of this.loggers) try {
378
+ logger.warn({ error: err }, `[autotel] ${message}`);
379
+ reported = true;
380
+ } catch {}
381
+ if (reported) return;
370
382
  }
371
- console.warn(`[autotel] ${message}: ${err}`);
383
+ try {
384
+ console.warn(`[autotel] ${message}: ${err}`);
385
+ } catch {}
372
386
  }
373
387
  async forceFlush() {}
374
388
  async shutdown() {}
@@ -22,8 +22,21 @@ interface KeepCondition {
22
22
  path?: string;
23
23
  }
24
24
  interface CanonicalLogLineOptions {
25
- /** Logger to use for emitting canonical log lines (defaults to OTel Logs API) */
26
- logger?: Logger;
25
+ /**
26
+ * Logger(s) to emit canonical log lines through. Pass an array to fan out to
27
+ * several. When omitted, lines go to the OTel Logs API instead.
28
+ */
29
+ logger?: Logger | Logger[];
30
+ /**
31
+ * Also emit through the OTel Logs API (and so to an OTLP logs backend such
32
+ * as Loki) alongside any `logger`.
33
+ *
34
+ * Defaults to true only when no `logger` is given, which is the historical
35
+ * either/or behaviour. Set it explicitly to have both: a console/pino logger
36
+ * keeps lines in the platform's own log view while OTLP carries the same
37
+ * lines to your backend.
38
+ */
39
+ otel?: boolean;
27
40
  /** Only emit canonical log lines for root spans (default: false) */
28
41
  rootSpansOnly?: boolean;
29
42
  /** Minimum log level for canonical log lines (default: 'info') */
@@ -116,7 +129,8 @@ interface CanonicalLogLineOptions {
116
129
  * ```
117
130
  */
118
131
  declare class CanonicalLogLineProcessor implements SpanProcessor {
119
- private logger?;
132
+ private loggers;
133
+ private useOtel;
120
134
  private rootSpansOnly;
121
135
  private minLevel;
122
136
  private messageFormat;
@@ -233,7 +233,8 @@ const SEVERITY_NUMBERS = /* @__PURE__ */ new Map([
233
233
  * ```
234
234
  */
235
235
  var CanonicalLogLineProcessor = class {
236
- logger;
236
+ loggers;
237
+ useOtel;
237
238
  rootSpansOnly;
238
239
  minLevel;
239
240
  messageFormat;
@@ -245,7 +246,8 @@ var CanonicalLogLineProcessor = class {
245
246
  pretty;
246
247
  getOTelLogger = null;
247
248
  constructor(options = {}) {
248
- this.logger = options.logger;
249
+ this.loggers = options.logger ? Array.isArray(options.logger) ? options.logger.filter(Boolean) : [options.logger] : [];
250
+ this.useOtel = options.otel ?? this.loggers.length === 0;
249
251
  this.rootSpansOnly = options.rootSpansOnly ?? false;
250
252
  this.minLevel = options.minLevel ?? "info";
251
253
  this.messageFormat = options.messageFormat ?? ((span) => `[${span.name}] Request completed`);
@@ -255,7 +257,7 @@ var CanonicalLogLineProcessor = class {
255
257
  this.drain = options.drain;
256
258
  this.onDrainError = options.onDrainError;
257
259
  this.pretty = options.pretty ?? (hasProcess() && process.env.NODE_ENV === "development");
258
- if (!this.logger) this.getOTelLogger = () => logs.getLogger("autotel.canonical-log-line");
260
+ if (this.useOtel) this.getOTelLogger = () => logs.getLogger("autotel.canonical-log-line");
259
261
  }
260
262
  buildKeepPredicate(keep) {
261
263
  if (!keep || keep.length === 0) return void 0;
@@ -287,10 +289,12 @@ var CanonicalLogLineProcessor = class {
287
289
  };
288
290
  if (this.shouldEmit && !this.shouldEmit(eventContext)) return;
289
291
  if (this.pretty) console.log(formatPrettyLogLine(eventContext));
290
- if (this.logger) this.emitViaLogger(level, message, canonicalLogLine);
291
- else if (this.getOTelLogger) {
292
+ if (this.loggers.length > 0) this.emitViaLogger(level, message, canonicalLogLine);
293
+ if (this.getOTelLogger) try {
292
294
  const otelLogger = this.getOTelLogger();
293
295
  this.emitViaOTel(level, message, canonicalLogLine, otelLogger);
296
+ } catch (error) {
297
+ this.reportInternalWarning("canonical log line OTel emission failed", error);
294
298
  }
295
299
  if (this.drain) Promise.resolve(this.drain(eventContext)).catch((error) => {
296
300
  if (this.onDrainError) {
@@ -328,7 +332,11 @@ var CanonicalLogLineProcessor = class {
328
332
  return redacted;
329
333
  }
330
334
  emitViaLogger(level, message, canonicalLogLine) {
331
- this.logger[level](canonicalLogLine, message);
335
+ for (const logger of this.loggers) try {
336
+ logger[level](canonicalLogLine, message);
337
+ } catch (error) {
338
+ this.reportInternalWarning("canonical log line logger emission failed", error);
339
+ }
332
340
  }
333
341
  emitViaOTel(level, message, canonicalLogLine, otelLogger) {
334
342
  const otelAttributes = {};
@@ -364,11 +372,17 @@ var CanonicalLogLineProcessor = class {
364
372
  }
365
373
  reportInternalWarning(message, cause) {
366
374
  const err = cause === void 0 ? "unknown error" : toError(cause).message;
367
- if (this.logger) {
368
- this.logger.warn({ error: err }, `[autotel] ${message}`);
369
- return;
375
+ if (this.loggers.length > 0) {
376
+ let reported = false;
377
+ for (const logger of this.loggers) try {
378
+ logger.warn({ error: err }, `[autotel] ${message}`);
379
+ reported = true;
380
+ } catch {}
381
+ if (reported) return;
370
382
  }
371
- console.warn(`[autotel] ${message}: ${err}`);
383
+ try {
384
+ console.warn(`[autotel] ${message}: ${err}`);
385
+ } catch {}
372
386
  }
373
387
  async forceFlush() {}
374
388
  async shutdown() {}
@@ -22,8 +22,21 @@ interface KeepCondition {
22
22
  path?: string;
23
23
  }
24
24
  interface CanonicalLogLineOptions {
25
- /** Logger to use for emitting canonical log lines (defaults to OTel Logs API) */
26
- logger?: Logger;
25
+ /**
26
+ * Logger(s) to emit canonical log lines through. Pass an array to fan out to
27
+ * several. When omitted, lines go to the OTel Logs API instead.
28
+ */
29
+ logger?: Logger | Logger[];
30
+ /**
31
+ * Also emit through the OTel Logs API (and so to an OTLP logs backend such
32
+ * as Loki) alongside any `logger`.
33
+ *
34
+ * Defaults to true only when no `logger` is given, which is the historical
35
+ * either/or behaviour. Set it explicitly to have both: a console/pino logger
36
+ * keeps lines in the platform's own log view while OTLP carries the same
37
+ * lines to your backend.
38
+ */
39
+ otel?: boolean;
27
40
  /** Only emit canonical log lines for root spans (default: false) */
28
41
  rootSpansOnly?: boolean;
29
42
  /** Minimum log level for canonical log lines (default: 'info') */
@@ -116,7 +129,8 @@ interface CanonicalLogLineOptions {
116
129
  * ```
117
130
  */
118
131
  declare class CanonicalLogLineProcessor implements SpanProcessor {
119
- private logger?;
132
+ private loggers;
133
+ private useOtel;
120
134
  private rootSpansOnly;
121
135
  private minLevel;
122
136
  private messageFormat;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_init = require('./init-DmM7dprX.cjs');
2
+ const require_init = require('./init-CNUkdwke.cjs');
3
3
 
4
4
  exports.CORRELATION_ID_BAGGAGE_KEY = require_init.CORRELATION_ID_BAGGAGE_KEY;
5
5
  exports.generateCorrelationId = require_init.generateCorrelationId;
@@ -1,3 +1,3 @@
1
- import { $ as getCorrelationStorage, Q as getCorrelationId, X as CORRELATION_ID_BAGGAGE_KEY, Z as generateCorrelationId, et as getOrCreateCorrelationId, nt as setCorrelationId, rt as setCorrelationIdInBaggage, tt as runWithCorrelationId } from "./init-CsKK9vZO.js";
1
+ import { $ as getCorrelationStorage, Q as getCorrelationId, X as CORRELATION_ID_BAGGAGE_KEY, Z as generateCorrelationId, et as getOrCreateCorrelationId, nt as setCorrelationId, rt as setCorrelationIdInBaggage, tt as runWithCorrelationId } from "./init-B2kv4bLO.js";
2
2
 
3
3
  export { CORRELATION_ID_BAGGAGE_KEY, generateCorrelationId, getCorrelationId, getCorrelationStorage, getOrCreateCorrelationId, runWithCorrelationId, setCorrelationId, setCorrelationIdInBaggage };
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_values = require('./values-DApyq53y.cjs');
3
3
  const require_config = require('./config.cjs');
4
- const require_init = require('./init-DmM7dprX.cjs');
4
+ const require_init = require('./init-CNUkdwke.cjs');
5
5
  let _opentelemetry_api = require("@opentelemetry/api");
6
6
 
7
7
  //#region src/decorators.ts
@@ -1,6 +1,6 @@
1
1
  import { y as splitNameOrOptions } from "./values-BenyvWKz.js";
2
2
  import { getConfig } from "./config.js";
3
- import { U as createTraceContext } from "./init-CsKK9vZO.js";
3
+ import { U as createTraceContext } from "./init-B2kv4bLO.js";
4
4
  import { SpanStatusCode } from "@opentelemetry/api";
5
5
 
6
6
  //#region src/decorators.ts
package/dist/event.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_init = require('./init-DmM7dprX.cjs');
2
+ const require_init = require('./init-CNUkdwke.cjs');
3
3
 
4
4
  exports.Event = require_init.Event;
5
5
  exports.getEvents = require_init.getEvents;
package/dist/event.js CHANGED
@@ -1,3 +1,3 @@
1
- import { d as Event, f as getEvents, p as resetEvents } from "./init-CsKK9vZO.js";
1
+ import { d as Event, f as getEvents, p as resetEvents } from "./init-B2kv4bLO.js";
2
2
 
3
3
  export { Event, getEvents, resetEvents };
@@ -0,0 +1,204 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _opentelemetry_api = require("@opentelemetry/api");
3
+ let _opentelemetry_api_logs = require("@opentelemetry/api-logs");
4
+
5
+ //#region src/feature-flags.ts
6
+ /**
7
+ * Feature flag evaluations, in the vocabulary OpenTelemetry already defined.
8
+ *
9
+ * A flagged rollout splits your traffic into two populations that share a
10
+ * service name, a route and a version. Without the flag on the span there is no
11
+ * way to ask the only question that matters during a rollout — is the new
12
+ * branch slower, or failing more, than the old one — and the usual workaround
13
+ * is to read it in the flag vendor's own dashboard, where the latency and the
14
+ * errors are not.
15
+ *
16
+ * The specification covers this: `feature_flag.key`, `.result.value`,
17
+ * `.result.variant`, `.result.reason`, `.provider.name`, `.context.id`, plus a
18
+ * `feature_flag.evaluation` event. Emitting those means any backend can split
19
+ * any metric by variant, with no vendor in the path.
20
+ *
21
+ * ## Attributes and events both
22
+ *
23
+ * Span attributes hold one flag: a second call overwrites the first. A request
24
+ * that branched on three flags needs one event each, which is what the
25
+ * `feature_flag.evaluation` event is for. Both are recorded — attributes so a
26
+ * single-flag span is filterable without unpacking events, events so a
27
+ * many-flag span keeps them all.
28
+ *
29
+ * ## Record what you branched on
30
+ *
31
+ * Record a flag where the code *reads* it, not where it is fetched. A flag
32
+ * evaluated and ignored explains nothing; the value the request actually took a
33
+ * branch on is the one that explains its behaviour.
34
+ */
35
+ /** Canonical `feature_flag.*` attribute keys. */
36
+ const FEATURE_FLAG = {
37
+ KEY: "feature_flag.key",
38
+ RESULT_VALUE: "feature_flag.result.value",
39
+ RESULT_VARIANT: "feature_flag.result.variant",
40
+ RESULT_REASON: "feature_flag.result.reason",
41
+ PROVIDER_NAME: "feature_flag.provider.name",
42
+ CONTEXT_ID: "feature_flag.context.id",
43
+ SET_ID: "feature_flag.set.id",
44
+ VERSION: "feature_flag.version",
45
+ /**
46
+ * Why an evaluation failed. `feature_flag.evaluation.error.message` is the
47
+ * deprecated spelling of this and is deliberately not emitted — writing both
48
+ * would double the cardinality to no benefit.
49
+ */
50
+ ERROR_MESSAGE: "feature_flag.error.message"
51
+ };
52
+ /** Canonical event name for one evaluation. */
53
+ const FEATURE_FLAG_EVALUATION_EVENT = "feature_flag.evaluation";
54
+ /**
55
+ * Canonical `feature_flag.result.reason` values. The registry defines them in
56
+ * lower snake case; OpenFeature and most SDKs report them upper-cased, and
57
+ * forwarding that splits every group-by into two buckets meaning the same
58
+ * thing.
59
+ */
60
+ const FEATURE_FLAG_REASON = [
61
+ "static",
62
+ "default",
63
+ "targeting_match",
64
+ "split",
65
+ "cached",
66
+ "disabled",
67
+ "unknown",
68
+ "stale",
69
+ "error"
70
+ ];
71
+ /**
72
+ * A flag value as an attribute.
73
+ *
74
+ * Booleans and numbers are kept as they are: `feature_flag.result.value`
75
+ * permits typed values, and stringifying `true` makes a boolean flag
76
+ * uncomparable with the numeric one next to it. Only a structured value — which
77
+ * no attribute type can hold — is serialised.
78
+ */
79
+ function asAttributeValue(value) {
80
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return value;
81
+ try {
82
+ return JSON.stringify(value) ?? String(value);
83
+ } catch {
84
+ return String(value);
85
+ }
86
+ }
87
+ /** Canonical attributes for one evaluation. Absent fields are omitted. */
88
+ function featureFlagAttributes(evaluation) {
89
+ const attributes = {
90
+ [FEATURE_FLAG.KEY]: evaluation.key,
91
+ [FEATURE_FLAG.RESULT_VALUE]: asAttributeValue(evaluation.value)
92
+ };
93
+ const optional = [
94
+ [FEATURE_FLAG.RESULT_VARIANT, evaluation.variant],
95
+ [FEATURE_FLAG.RESULT_REASON, evaluation.reason === void 0 ? void 0 : evaluation.reason.toLowerCase()],
96
+ [FEATURE_FLAG.PROVIDER_NAME, evaluation.provider],
97
+ [FEATURE_FLAG.CONTEXT_ID, evaluation.contextId],
98
+ [FEATURE_FLAG.SET_ID, evaluation.setId],
99
+ [FEATURE_FLAG.VERSION, evaluation.version],
100
+ [FEATURE_FLAG.ERROR_MESSAGE, evaluation.errorMessage]
101
+ ];
102
+ for (const [key, value] of optional) if (value !== void 0) attributes[key] = value;
103
+ return attributes;
104
+ }
105
+ /**
106
+ * Record a flag evaluation on `sink` — as attributes and as a
107
+ * `feature_flag.evaluation` event.
108
+ *
109
+ * A missing sink is a no-op: instrumentation must never be the reason a branch
110
+ * throws, and a flag read outside any span is a legitimate thing to do.
111
+ */
112
+ function recordFeatureFlag(sink, evaluation) {
113
+ if (!sink) return;
114
+ const attributes = featureFlagAttributes(evaluation);
115
+ sink.setAttributes(attributes);
116
+ sink.track?.(FEATURE_FLAG_EVALUATION_EVENT, attributes);
117
+ }
118
+ /**
119
+ * Emit the evaluation as an OpenTelemetry **log record**.
120
+ *
121
+ * This is the Logs API model the repository emits events through. The obvious
122
+ * shortcut — `Span.addEvent` on the active span — puts the evaluation somewhere
123
+ * no log or event pipeline will ever look, which is exactly the failure the
124
+ * invariant exists to prevent. `correlated-events` is not a way around it
125
+ * either: that helper prefers `addEvent` when the target has one, so routing
126
+ * through it lands in the same place with more indirection.
127
+ *
128
+ * `logs.getLogger` returns a no-op logger when no `LoggerProvider` is
129
+ * registered, so this costs nothing in an application that exports no logs.
130
+ */
131
+ function emitEvaluationRecord(attributes) {
132
+ _opentelemetry_api_logs.logs.getLogger("autotel").emit({
133
+ eventName: FEATURE_FLAG_EVALUATION_EVENT,
134
+ attributes
135
+ });
136
+ }
137
+ function evaluationFrom(hookContext, value, extra) {
138
+ return {
139
+ key: hookContext.flagKey,
140
+ value,
141
+ provider: hookContext.providerMetadata?.name,
142
+ contextId: hookContext.context?.targetingKey,
143
+ setId: hookContext.clientMetadata?.name,
144
+ ...extra
145
+ };
146
+ }
147
+ /**
148
+ * An OpenFeature hook that records every evaluation under the canonical
149
+ * `feature_flag.*` convention.
150
+ *
151
+ * This is the zero-config path: OpenFeature already sits between the
152
+ * application and whichever flag vendor it uses, and it already fires on every
153
+ * evaluation — which is exactly the moment worth recording, because it is the
154
+ * moment the code branched.
155
+ *
156
+ * ```ts
157
+ * import { OpenFeature } from '@openfeature/server-sdk';
158
+ * import { autotelOpenFeatureHook } from 'autotel/feature-flags';
159
+ *
160
+ * OpenFeature.addHooks(autotelOpenFeatureHook());
161
+ * ```
162
+ *
163
+ * A failed evaluation is still recorded, with the default value the code
164
+ * actually used and `reason: 'ERROR'`. The default is what the request behaved
165
+ * as; the failure is why.
166
+ */
167
+ function autotelOpenFeatureHook(options = {}) {
168
+ const getSpan = options.getSpan ?? (() => _opentelemetry_api.trace.getActiveSpan());
169
+ const emitLogRecord = options.emitLogRecord ?? emitEvaluationRecord;
170
+ /**
171
+ * Attributes onto the span that branched, and the event wherever the sink
172
+ * says. A sink with no `track` of its own gets a log record — including when
173
+ * there is no span at all, because the evaluation still happened.
174
+ */
175
+ const record = (evaluation) => {
176
+ const sink = getSpan();
177
+ const attributes = featureFlagAttributes(evaluation);
178
+ sink?.setAttributes(attributes);
179
+ if (sink?.track) sink.track(FEATURE_FLAG_EVALUATION_EVENT, attributes);
180
+ else emitLogRecord(attributes);
181
+ };
182
+ return {
183
+ after(hookContext, details) {
184
+ record(evaluationFrom(hookContext, details.value, {
185
+ variant: details.variant,
186
+ reason: details.reason
187
+ }));
188
+ },
189
+ error(hookContext, error) {
190
+ record(evaluationFrom(hookContext, hookContext.defaultValue, {
191
+ reason: "error",
192
+ errorMessage: error instanceof Error ? error.message : String(error)
193
+ }));
194
+ }
195
+ };
196
+ }
197
+
198
+ //#endregion
199
+ exports.FEATURE_FLAG = FEATURE_FLAG;
200
+ exports.FEATURE_FLAG_EVALUATION_EVENT = FEATURE_FLAG_EVALUATION_EVENT;
201
+ exports.FEATURE_FLAG_REASON = FEATURE_FLAG_REASON;
202
+ exports.autotelOpenFeatureHook = autotelOpenFeatureHook;
203
+ exports.featureFlagAttributes = featureFlagAttributes;
204
+ exports.recordFeatureFlag = recordFeatureFlag;