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.
- package/README.md +17 -0
- package/dist/auto.cjs +1 -1
- package/dist/auto.js +1 -1
- package/dist/{canonical-log-line-processor-DolzdawH.cjs → canonical-log-line-processor-B-tzezhk.cjs} +24 -10
- package/dist/{canonical-log-line-processor-l0Aqe6yp.d.cts → canonical-log-line-processor-C0Z_N4oL.d.cts} +17 -3
- package/dist/{canonical-log-line-processor-hFUmS__K.js → canonical-log-line-processor-CBv-OCp4.js} +24 -10
- package/dist/{canonical-log-line-processor-cPB4Axk-.d.ts → canonical-log-line-processor-Yq4cXcD5.d.ts} +17 -3
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.js +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/feature-flags.cjs +204 -0
- package/dist/feature-flags.d.cts +175 -0
- package/dist/feature-flags.d.ts +175 -0
- package/dist/feature-flags.js +198 -0
- package/dist/{functional-C4js2vTM.js → functional-B86zszmZ.js} +1 -1
- package/dist/{functional-DHlD7V0x.cjs → functional-BOZ3me6w.cjs} +1 -1
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{init-CsKK9vZO.js → init-B2kv4bLO.js} +31 -12
- package/dist/{init-8GSy2bza.d.cts → init-BU7qa-Ii.d.cts} +67 -6
- package/dist/{init-DmM7dprX.cjs → init-CNUkdwke.cjs} +31 -12
- package/dist/{init-UnKrXhpq.d.ts → init-DDCgmfa4.d.ts} +67 -6
- package/dist/instrumentation.cjs +1 -1
- package/dist/instrumentation.js +1 -1
- package/dist/messaging.cjs +2 -2
- package/dist/messaging.js +2 -2
- package/dist/processors.cjs +1 -1
- package/dist/processors.d.cts +1 -1
- package/dist/processors.d.ts +1 -1
- package/dist/processors.js +1 -1
- package/dist/sampling.cjs +1 -1
- package/dist/sampling.js +1 -1
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/tail-sampling-processor.cjs +1 -1
- package/dist/tail-sampling-processor.js +1 -1
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/dist/yaml-config.cjs +1 -1
- package/dist/yaml-config.d.cts +1 -1
- package/dist/yaml-config.d.ts +1 -1
- package/dist/yaml-config.js +1 -1
- 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-
|
|
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
package/dist/{canonical-log-line-processor-DolzdawH.cjs → canonical-log-line-processor-B-tzezhk.cjs}
RENAMED
|
@@ -233,7 +233,8 @@ const SEVERITY_NUMBERS = /* @__PURE__ */ new Map([
|
|
|
233
233
|
* ```
|
|
234
234
|
*/
|
|
235
235
|
var CanonicalLogLineProcessor = class {
|
|
236
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
291
|
-
|
|
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.
|
|
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.
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
26
|
-
|
|
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
|
|
132
|
+
private loggers;
|
|
133
|
+
private useOtel;
|
|
120
134
|
private rootSpansOnly;
|
|
121
135
|
private minLevel;
|
|
122
136
|
private messageFormat;
|
package/dist/{canonical-log-line-processor-hFUmS__K.js → canonical-log-line-processor-CBv-OCp4.js}
RENAMED
|
@@ -233,7 +233,8 @@ const SEVERITY_NUMBERS = /* @__PURE__ */ new Map([
|
|
|
233
233
|
* ```
|
|
234
234
|
*/
|
|
235
235
|
var CanonicalLogLineProcessor = class {
|
|
236
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
291
|
-
|
|
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.
|
|
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.
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
26
|
-
|
|
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
|
|
132
|
+
private loggers;
|
|
133
|
+
private useOtel;
|
|
120
134
|
private rootSpansOnly;
|
|
121
135
|
private minLevel;
|
|
122
136
|
private messageFormat;
|
package/dist/correlation-id.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_init = require('./init-
|
|
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;
|
package/dist/correlation-id.js
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/decorators.cjs
CHANGED
|
@@ -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-
|
|
4
|
+
const require_init = require('./init-CNUkdwke.cjs');
|
|
5
5
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
6
6
|
|
|
7
7
|
//#region src/decorators.ts
|
package/dist/decorators.js
CHANGED
|
@@ -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-
|
|
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
package/dist/event.js
CHANGED
|
@@ -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;
|