autotel 7.0.1 → 7.2.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 +25 -0
- package/dist/analysis.cjs +17 -0
- package/dist/analysis.d.cts +9 -1
- package/dist/analysis.d.ts +9 -1
- package/dist/analysis.js +17 -1
- package/dist/auto.cjs +2 -3
- package/dist/auto.js +1 -2
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.d.cts +1 -1
- package/dist/decorators.d.ts +1 -1
- package/dist/decorators.js +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/evidence.cjs +57 -0
- package/dist/evidence.d.cts +61 -0
- package/dist/evidence.d.ts +61 -0
- package/dist/evidence.js +50 -0
- package/dist/{functional-C2ONG9PK.cjs → functional-C925G95W.cjs} +68 -17
- package/dist/{functional-CJn2zY4o.d.cts → functional-CgWcRXGX.d.cts} +12 -5
- package/dist/{functional-nS5U4KCQ.js → functional-Doi0hFm3.js} +64 -13
- package/dist/{functional-DXy1cHFF.d.ts → functional-RFFpQzAV.d.ts} +12 -5
- package/dist/functional.cjs +1 -1
- package/dist/functional.d.cts +1 -1
- package/dist/functional.d.ts +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +53 -15
- package/dist/index.d.cts +24 -4
- package/dist/index.d.ts +24 -4
- package/dist/index.js +40 -5
- package/dist/{init-DrlL1eis.d.cts → init-8GSy2bza.d.cts} +1 -1
- package/dist/{init-DDyaLzeg.js → init-DzAdgcFM.js} +2181 -1303
- package/dist/{init-BgnYi2sm.d.ts → init-UnKrXhpq.d.ts} +1 -1
- package/dist/{init-C5aHRqi2.cjs → init-aTZKsAwy.cjs} +2324 -1294
- package/dist/instrumentation.cjs +2 -3
- package/dist/instrumentation.js +1 -2
- package/dist/messaging.cjs +5 -5
- package/dist/messaging.js +2 -2
- package/dist/{sampling-CM5S2Lp3.d.ts → sampling-DBng4Foa.d.ts} +20 -1
- package/dist/{sampling-CmRdnXUi.d.cts → sampling-Er217Mge.d.cts} +20 -1
- package/dist/sampling.cjs +22 -601
- package/dist/sampling.d.cts +2 -2
- package/dist/sampling.d.ts +2 -2
- package/dist/sampling.js +2 -585
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/tail-sampling-processor.cjs +2 -26
- package/dist/tail-sampling-processor.js +1 -25
- 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 +4 -248
- package/dist/yaml-config.d.cts +2 -2
- package/dist/yaml-config.d.ts +2 -2
- package/dist/yaml-config.js +1 -242
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -775,6 +775,31 @@ app.use((req, res, next) => {
|
|
|
775
775
|
- Baggage values are strings (convert numbers/objects before setting)
|
|
776
776
|
- Never put PII in baggage - it propagates in HTTP headers across services!
|
|
777
777
|
|
|
778
|
+
### Getting the trace id, and a link to it
|
|
779
|
+
|
|
780
|
+
`getActiveTraceContext()` reads the trace currently in scope, and
|
|
781
|
+
`resolveTraceUrl()` turns its id into a link to wherever you view traces. Printing
|
|
782
|
+
that link beside a slow request is the shortest path from "this was slow" to
|
|
783
|
+
looking at why:
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
import { getActiveTraceContext, resolveTraceUrl, span } from 'autotel';
|
|
787
|
+
|
|
788
|
+
await span('GET /feed', async () => {
|
|
789
|
+
const traceId = getActiveTraceContext()?.traceId;
|
|
790
|
+
const url = resolveTraceUrl(
|
|
791
|
+
'http://localhost:4318/#trace={traceId}',
|
|
792
|
+
traceId,
|
|
793
|
+
);
|
|
794
|
+
console.log(`feed request ${url ?? traceId}`);
|
|
795
|
+
});
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
`resolveTraceUrl(template, traceId)` substitutes `{traceId}`. Pass `undefined`
|
|
799
|
+
as the template and it falls back to `OTEL_TRACE_URL_TEMPLATE`, so the same code
|
|
800
|
+
points at devtools locally and at your vendor in production without a branch.
|
|
801
|
+
It returns `undefined` when neither is set.
|
|
802
|
+
|
|
778
803
|
### Reusable Middleware Helpers
|
|
779
804
|
|
|
780
805
|
- `withTracing(options)` : create a preconfigured wrapper (service name, default attributes, skip rules).
|
package/dist/analysis.cjs
CHANGED
|
@@ -81,6 +81,23 @@ function compareCohorts(options) {
|
|
|
81
81
|
results.sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference) || b.difference - a.difference || a.field.localeCompare(b.field) || a.value.localeCompare(b.value));
|
|
82
82
|
return results.slice(0, limit);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Label a numeric value with the range it falls in.
|
|
86
|
+
*
|
|
87
|
+
* `compareCohorts` skips a field whose values never repeat, which is what a
|
|
88
|
+
* raw duration or byte count does. Bucketing at instrumentation time turns
|
|
89
|
+
* such a field into one that can describe a cohort.
|
|
90
|
+
*/
|
|
91
|
+
function bucket(value, boundaries) {
|
|
92
|
+
if (!Number.isFinite(value) || boundaries.length === 0) return "unknown";
|
|
93
|
+
const ordered = [...boundaries].sort((a, b) => a - b);
|
|
94
|
+
for (const [index, boundary] of ordered.entries()) if (value < boundary) {
|
|
95
|
+
const lower = ordered[index - 1];
|
|
96
|
+
return lower === void 0 ? `<${boundary}` : `${lower}-${boundary}`;
|
|
97
|
+
}
|
|
98
|
+
return `>=${ordered.at(-1)}`;
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
//#endregion
|
|
102
|
+
exports.bucket = bucket;
|
|
86
103
|
exports.compareCohorts = compareCohorts;
|
package/dist/analysis.d.cts
CHANGED
|
@@ -82,5 +82,13 @@ interface CompareCohortsOptions {
|
|
|
82
82
|
* @returns Differences sorted by absolute strength, strongest first
|
|
83
83
|
*/
|
|
84
84
|
declare function compareCohorts(options: CompareCohortsOptions): CohortDifference[];
|
|
85
|
+
/**
|
|
86
|
+
* Label a numeric value with the range it falls in.
|
|
87
|
+
*
|
|
88
|
+
* `compareCohorts` skips a field whose values never repeat, which is what a
|
|
89
|
+
* raw duration or byte count does. Bucketing at instrumentation time turns
|
|
90
|
+
* such a field into one that can describe a cohort.
|
|
91
|
+
*/
|
|
92
|
+
declare function bucket(value: number, boundaries: readonly number[]): string;
|
|
85
93
|
//#endregion
|
|
86
|
-
export { AnalysisEvent, CohortDifference, CompareCohortsOptions, compareCohorts };
|
|
94
|
+
export { AnalysisEvent, CohortDifference, CompareCohortsOptions, bucket, compareCohorts };
|
package/dist/analysis.d.ts
CHANGED
|
@@ -82,5 +82,13 @@ interface CompareCohortsOptions {
|
|
|
82
82
|
* @returns Differences sorted by absolute strength, strongest first
|
|
83
83
|
*/
|
|
84
84
|
declare function compareCohorts(options: CompareCohortsOptions): CohortDifference[];
|
|
85
|
+
/**
|
|
86
|
+
* Label a numeric value with the range it falls in.
|
|
87
|
+
*
|
|
88
|
+
* `compareCohorts` skips a field whose values never repeat, which is what a
|
|
89
|
+
* raw duration or byte count does. Bucketing at instrumentation time turns
|
|
90
|
+
* such a field into one that can describe a cohort.
|
|
91
|
+
*/
|
|
92
|
+
declare function bucket(value: number, boundaries: readonly number[]): string;
|
|
85
93
|
//#endregion
|
|
86
|
-
export { AnalysisEvent, CohortDifference, CompareCohortsOptions, compareCohorts };
|
|
94
|
+
export { AnalysisEvent, CohortDifference, CompareCohortsOptions, bucket, compareCohorts };
|
package/dist/analysis.js
CHANGED
|
@@ -79,6 +79,22 @@ function compareCohorts(options) {
|
|
|
79
79
|
results.sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference) || b.difference - a.difference || a.field.localeCompare(b.field) || a.value.localeCompare(b.value));
|
|
80
80
|
return results.slice(0, limit);
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Label a numeric value with the range it falls in.
|
|
84
|
+
*
|
|
85
|
+
* `compareCohorts` skips a field whose values never repeat, which is what a
|
|
86
|
+
* raw duration or byte count does. Bucketing at instrumentation time turns
|
|
87
|
+
* such a field into one that can describe a cohort.
|
|
88
|
+
*/
|
|
89
|
+
function bucket(value, boundaries) {
|
|
90
|
+
if (!Number.isFinite(value) || boundaries.length === 0) return "unknown";
|
|
91
|
+
const ordered = [...boundaries].sort((a, b) => a - b);
|
|
92
|
+
for (const [index, boundary] of ordered.entries()) if (value < boundary) {
|
|
93
|
+
const lower = ordered[index - 1];
|
|
94
|
+
return lower === void 0 ? `<${boundary}` : `${lower}-${boundary}`;
|
|
95
|
+
}
|
|
96
|
+
return `>=${ordered.at(-1)}`;
|
|
97
|
+
}
|
|
82
98
|
|
|
83
99
|
//#endregion
|
|
84
|
-
export { compareCohorts };
|
|
100
|
+
export { bucket, compareCohorts };
|
package/dist/auto.cjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
|
|
2
|
-
const require_init = require('./init-
|
|
3
|
-
const require_yaml_config = require('./yaml-config.cjs');
|
|
2
|
+
const require_init = require('./init-aTZKsAwy.cjs');
|
|
4
3
|
let node_module = require("node:module");
|
|
5
4
|
node_module = require_rolldown_runtime.__toESM(node_module, 1);
|
|
6
5
|
let import_in_the_middle = require("import-in-the-middle");
|
|
@@ -42,7 +41,7 @@ let import_in_the_middle = require("import-in-the-middle");
|
|
|
42
41
|
*/
|
|
43
42
|
const { registerOptions } = (0, import_in_the_middle.createAddHookMessageChannel)();
|
|
44
43
|
node_module.register("import-in-the-middle/hook.mjs", require("url").pathToFileURL(__filename).href, registerOptions);
|
|
45
|
-
const yamlConfig =
|
|
44
|
+
const yamlConfig = require_init.loadYamlConfig();
|
|
46
45
|
const autoInstrumentationsEnv = process.env.AUTOTEL_INTEGRATIONS;
|
|
47
46
|
const autoInstrumentations = autoInstrumentationsEnv === "true" ? true : autoInstrumentationsEnv ? autoInstrumentationsEnv.split(",").map((s) => s.trim()) : yamlConfig?.autoInstrumentations ?? ["http", "express"];
|
|
48
47
|
require_init.init({
|
package/dist/auto.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { i as init } from "./init-
|
|
2
|
-
import { loadYamlConfig } from "./yaml-config.js";
|
|
1
|
+
import { g as loadYamlConfig, i as init } from "./init-DzAdgcFM.js";
|
|
3
2
|
import * as nodeModule from "node:module";
|
|
4
3
|
import { createAddHookMessageChannel } from "import-in-the-middle";
|
|
5
4
|
|
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-aTZKsAwy.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 {
|
|
1
|
+
import { $ as getOrCreateCorrelationId, Q as getCorrelationStorage, X as generateCorrelationId, Y as CORRELATION_ID_BAGGAGE_KEY, Z as getCorrelationId, et as runWithCorrelationId, nt as setCorrelationIdInBaggage, tt as setCorrelationId } from "./init-DzAdgcFM.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-aTZKsAwy.cjs');
|
|
5
5
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
6
6
|
|
|
7
7
|
//#region src/decorators.ts
|
package/dist/decorators.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as TraceContext } from "./trace-context-C1Cn1q0y.cjs";
|
|
2
|
-
import { m as TracingOptions } from "./functional-
|
|
2
|
+
import { m as TracingOptions } from "./functional-CgWcRXGX.cjs";
|
|
3
3
|
//#region src/decorators.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Options for @Trace method decorator
|
package/dist/decorators.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as TraceContext } from "./trace-context-f89xPJbK.js";
|
|
2
|
-
import { m as TracingOptions } from "./functional-
|
|
2
|
+
import { m as TracingOptions } from "./functional-RFFpQzAV.js";
|
|
3
3
|
//#region src/decorators.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Options for @Trace method decorator
|
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 {
|
|
3
|
+
import { H as createTraceContext } from "./init-DzAdgcFM.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,57 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/evidence.ts
|
|
4
|
+
/** Prefix for per-field evidence labels. */
|
|
5
|
+
const EVIDENCE_ATTR_PREFIX = "autotel.evidence.";
|
|
6
|
+
/** The span attribute key carrying `field`'s evidence label. */
|
|
7
|
+
function evidenceAttribute(field) {
|
|
8
|
+
return `${EVIDENCE_ATTR_PREFIX}${field}`;
|
|
9
|
+
}
|
|
10
|
+
/** Label how `field` on this span came to be. */
|
|
11
|
+
function recordEvidence(ctx, field, quality) {
|
|
12
|
+
ctx.setAttribute(evidenceAttribute(field), quality);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The capture surfaces a deployment can be asked about. Closed set, because the
|
|
16
|
+
* point is that a backend can query `unobserved` across services and get
|
|
17
|
+
* comparable answers.
|
|
18
|
+
*/
|
|
19
|
+
const CAPTURE_SURFACES = [
|
|
20
|
+
"llm_calls",
|
|
21
|
+
"tool_calls",
|
|
22
|
+
"user_prompts",
|
|
23
|
+
"file_io",
|
|
24
|
+
"subprocess",
|
|
25
|
+
"network",
|
|
26
|
+
"ide_context"
|
|
27
|
+
];
|
|
28
|
+
const CAPTURE_COVERAGE_ATTR = {
|
|
29
|
+
observed: "autotel.coverage.observed",
|
|
30
|
+
unobserved: "autotel.coverage.unobserved"
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resource attributes declaring {@link CaptureCoverage}.
|
|
34
|
+
*
|
|
35
|
+
* An empty list is omitted rather than emitted: `unobserved: []` would read as
|
|
36
|
+
* "this process has no blind spots", which is a stronger claim than declaring
|
|
37
|
+
* nothing. A surface claimed as both is dropped from both — a contradiction
|
|
38
|
+
* that left `observed` intact would let a reader conclude a gap does not exist.
|
|
39
|
+
*/
|
|
40
|
+
function captureCoverageAttributes(coverage) {
|
|
41
|
+
const claimedBoth = new Set(coverage.observed.filter((s) => coverage.unobserved.includes(s)));
|
|
42
|
+
const keep = (surfaces) => [...new Set(surfaces.filter((s) => !claimedBoth.has(s)))];
|
|
43
|
+
const observed = keep(coverage.observed);
|
|
44
|
+
const unobserved = keep(coverage.unobserved);
|
|
45
|
+
return {
|
|
46
|
+
...observed.length > 0 && { [CAPTURE_COVERAGE_ATTR.observed]: observed },
|
|
47
|
+
...unobserved.length > 0 && { [CAPTURE_COVERAGE_ATTR.unobserved]: unobserved }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
exports.CAPTURE_COVERAGE_ATTR = CAPTURE_COVERAGE_ATTR;
|
|
53
|
+
exports.CAPTURE_SURFACES = CAPTURE_SURFACES;
|
|
54
|
+
exports.EVIDENCE_ATTR_PREFIX = EVIDENCE_ATTR_PREFIX;
|
|
55
|
+
exports.captureCoverageAttributes = captureCoverageAttributes;
|
|
56
|
+
exports.evidenceAttribute = evidenceAttribute;
|
|
57
|
+
exports.recordEvidence = recordEvidence;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Attributes } from "@opentelemetry/api";
|
|
2
|
+
//#region src/evidence.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Why a recorded fact is the way it is.
|
|
5
|
+
*
|
|
6
|
+
* There is deliberately no `unknown` member: a field carrying no evidence label
|
|
7
|
+
* is already unknown, and encoding that as a value invites code that treats an
|
|
8
|
+
* unlabelled field as *verified* unknown rather than simply unexamined.
|
|
9
|
+
*/
|
|
10
|
+
type EvidenceQuality =
|
|
11
|
+
/** The instrumentation saw the value directly. */
|
|
12
|
+
'observed' |
|
|
13
|
+
/** Derived from other observed facts, not seen. */
|
|
14
|
+
'inferred' |
|
|
15
|
+
/** Computed from a price table, heuristic, or model — not reported upstream. */
|
|
16
|
+
'estimated' |
|
|
17
|
+
/** Seen, then cut short. What is here is real; what is missing is not. */
|
|
18
|
+
'truncated' |
|
|
19
|
+
/** Seen, then removed before storage. */
|
|
20
|
+
'redacted' |
|
|
21
|
+
/** Expected here and not present — a real gap in an observable place. */
|
|
22
|
+
'absent' |
|
|
23
|
+
/** This process cannot see it at all, so its absence proves nothing. */
|
|
24
|
+
'unobservable';
|
|
25
|
+
/** Prefix for per-field evidence labels. */
|
|
26
|
+
declare const EVIDENCE_ATTR_PREFIX = "autotel.evidence.";
|
|
27
|
+
/** The span attribute key carrying `field`'s evidence label. */
|
|
28
|
+
declare function evidenceAttribute(field: string): string;
|
|
29
|
+
/** The one thing {@link recordEvidence} needs — any `TraceContext` or `Span`. */
|
|
30
|
+
interface EvidenceTarget {
|
|
31
|
+
setAttribute(key: string, value: string): void;
|
|
32
|
+
}
|
|
33
|
+
/** Label how `field` on this span came to be. */
|
|
34
|
+
declare function recordEvidence(ctx: EvidenceTarget, field: string, quality: EvidenceQuality): void;
|
|
35
|
+
/**
|
|
36
|
+
* The capture surfaces a deployment can be asked about. Closed set, because the
|
|
37
|
+
* point is that a backend can query `unobserved` across services and get
|
|
38
|
+
* comparable answers.
|
|
39
|
+
*/
|
|
40
|
+
declare const CAPTURE_SURFACES: readonly ["llm_calls", "tool_calls", "user_prompts", "file_io", "subprocess", "network", "ide_context"];
|
|
41
|
+
type CaptureSurface = (typeof CAPTURE_SURFACES)[number];
|
|
42
|
+
/** What this process can and cannot see. Anything in neither list is unknown. */
|
|
43
|
+
interface CaptureCoverage {
|
|
44
|
+
observed: readonly CaptureSurface[];
|
|
45
|
+
unobserved: readonly CaptureSurface[];
|
|
46
|
+
}
|
|
47
|
+
declare const CAPTURE_COVERAGE_ATTR: {
|
|
48
|
+
readonly observed: "autotel.coverage.observed";
|
|
49
|
+
readonly unobserved: "autotel.coverage.unobserved";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Resource attributes declaring {@link CaptureCoverage}.
|
|
53
|
+
*
|
|
54
|
+
* An empty list is omitted rather than emitted: `unobserved: []` would read as
|
|
55
|
+
* "this process has no blind spots", which is a stronger claim than declaring
|
|
56
|
+
* nothing. A surface claimed as both is dropped from both — a contradiction
|
|
57
|
+
* that left `observed` intact would let a reader conclude a gap does not exist.
|
|
58
|
+
*/
|
|
59
|
+
declare function captureCoverageAttributes(coverage: CaptureCoverage): Attributes;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { CAPTURE_COVERAGE_ATTR, CAPTURE_SURFACES, CaptureCoverage, CaptureSurface, EVIDENCE_ATTR_PREFIX, EvidenceQuality, EvidenceTarget, captureCoverageAttributes, evidenceAttribute, recordEvidence };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Attributes } from "@opentelemetry/api";
|
|
2
|
+
//#region src/evidence.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Why a recorded fact is the way it is.
|
|
5
|
+
*
|
|
6
|
+
* There is deliberately no `unknown` member: a field carrying no evidence label
|
|
7
|
+
* is already unknown, and encoding that as a value invites code that treats an
|
|
8
|
+
* unlabelled field as *verified* unknown rather than simply unexamined.
|
|
9
|
+
*/
|
|
10
|
+
type EvidenceQuality =
|
|
11
|
+
/** The instrumentation saw the value directly. */
|
|
12
|
+
'observed' |
|
|
13
|
+
/** Derived from other observed facts, not seen. */
|
|
14
|
+
'inferred' |
|
|
15
|
+
/** Computed from a price table, heuristic, or model — not reported upstream. */
|
|
16
|
+
'estimated' |
|
|
17
|
+
/** Seen, then cut short. What is here is real; what is missing is not. */
|
|
18
|
+
'truncated' |
|
|
19
|
+
/** Seen, then removed before storage. */
|
|
20
|
+
'redacted' |
|
|
21
|
+
/** Expected here and not present — a real gap in an observable place. */
|
|
22
|
+
'absent' |
|
|
23
|
+
/** This process cannot see it at all, so its absence proves nothing. */
|
|
24
|
+
'unobservable';
|
|
25
|
+
/** Prefix for per-field evidence labels. */
|
|
26
|
+
declare const EVIDENCE_ATTR_PREFIX = "autotel.evidence.";
|
|
27
|
+
/** The span attribute key carrying `field`'s evidence label. */
|
|
28
|
+
declare function evidenceAttribute(field: string): string;
|
|
29
|
+
/** The one thing {@link recordEvidence} needs — any `TraceContext` or `Span`. */
|
|
30
|
+
interface EvidenceTarget {
|
|
31
|
+
setAttribute(key: string, value: string): void;
|
|
32
|
+
}
|
|
33
|
+
/** Label how `field` on this span came to be. */
|
|
34
|
+
declare function recordEvidence(ctx: EvidenceTarget, field: string, quality: EvidenceQuality): void;
|
|
35
|
+
/**
|
|
36
|
+
* The capture surfaces a deployment can be asked about. Closed set, because the
|
|
37
|
+
* point is that a backend can query `unobserved` across services and get
|
|
38
|
+
* comparable answers.
|
|
39
|
+
*/
|
|
40
|
+
declare const CAPTURE_SURFACES: readonly ["llm_calls", "tool_calls", "user_prompts", "file_io", "subprocess", "network", "ide_context"];
|
|
41
|
+
type CaptureSurface = (typeof CAPTURE_SURFACES)[number];
|
|
42
|
+
/** What this process can and cannot see. Anything in neither list is unknown. */
|
|
43
|
+
interface CaptureCoverage {
|
|
44
|
+
observed: readonly CaptureSurface[];
|
|
45
|
+
unobserved: readonly CaptureSurface[];
|
|
46
|
+
}
|
|
47
|
+
declare const CAPTURE_COVERAGE_ATTR: {
|
|
48
|
+
readonly observed: "autotel.coverage.observed";
|
|
49
|
+
readonly unobserved: "autotel.coverage.unobserved";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Resource attributes declaring {@link CaptureCoverage}.
|
|
53
|
+
*
|
|
54
|
+
* An empty list is omitted rather than emitted: `unobserved: []` would read as
|
|
55
|
+
* "this process has no blind spots", which is a stronger claim than declaring
|
|
56
|
+
* nothing. A surface claimed as both is dropped from both — a contradiction
|
|
57
|
+
* that left `observed` intact would let a reader conclude a gap does not exist.
|
|
58
|
+
*/
|
|
59
|
+
declare function captureCoverageAttributes(coverage: CaptureCoverage): Attributes;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { CAPTURE_COVERAGE_ATTR, CAPTURE_SURFACES, CaptureCoverage, CaptureSurface, EVIDENCE_ATTR_PREFIX, EvidenceQuality, EvidenceTarget, captureCoverageAttributes, evidenceAttribute, recordEvidence };
|
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/evidence.ts
|
|
2
|
+
/** Prefix for per-field evidence labels. */
|
|
3
|
+
const EVIDENCE_ATTR_PREFIX = "autotel.evidence.";
|
|
4
|
+
/** The span attribute key carrying `field`'s evidence label. */
|
|
5
|
+
function evidenceAttribute(field) {
|
|
6
|
+
return `${EVIDENCE_ATTR_PREFIX}${field}`;
|
|
7
|
+
}
|
|
8
|
+
/** Label how `field` on this span came to be. */
|
|
9
|
+
function recordEvidence(ctx, field, quality) {
|
|
10
|
+
ctx.setAttribute(evidenceAttribute(field), quality);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The capture surfaces a deployment can be asked about. Closed set, because the
|
|
14
|
+
* point is that a backend can query `unobserved` across services and get
|
|
15
|
+
* comparable answers.
|
|
16
|
+
*/
|
|
17
|
+
const CAPTURE_SURFACES = [
|
|
18
|
+
"llm_calls",
|
|
19
|
+
"tool_calls",
|
|
20
|
+
"user_prompts",
|
|
21
|
+
"file_io",
|
|
22
|
+
"subprocess",
|
|
23
|
+
"network",
|
|
24
|
+
"ide_context"
|
|
25
|
+
];
|
|
26
|
+
const CAPTURE_COVERAGE_ATTR = {
|
|
27
|
+
observed: "autotel.coverage.observed",
|
|
28
|
+
unobserved: "autotel.coverage.unobserved"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Resource attributes declaring {@link CaptureCoverage}.
|
|
32
|
+
*
|
|
33
|
+
* An empty list is omitted rather than emitted: `unobserved: []` would read as
|
|
34
|
+
* "this process has no blind spots", which is a stronger claim than declaring
|
|
35
|
+
* nothing. A surface claimed as both is dropped from both — a contradiction
|
|
36
|
+
* that left `observed` intact would let a reader conclude a gap does not exist.
|
|
37
|
+
*/
|
|
38
|
+
function captureCoverageAttributes(coverage) {
|
|
39
|
+
const claimedBoth = new Set(coverage.observed.filter((s) => coverage.unobserved.includes(s)));
|
|
40
|
+
const keep = (surfaces) => [...new Set(surfaces.filter((s) => !claimedBoth.has(s)))];
|
|
41
|
+
const observed = keep(coverage.observed);
|
|
42
|
+
const unobserved = keep(coverage.unobserved);
|
|
43
|
+
return {
|
|
44
|
+
...observed.length > 0 && { [CAPTURE_COVERAGE_ATTR.observed]: observed },
|
|
45
|
+
...unobserved.length > 0 && { [CAPTURE_COVERAGE_ATTR.unobserved]: unobserved }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CAPTURE_COVERAGE_ATTR, CAPTURE_SURFACES, EVIDENCE_ATTR_PREFIX, captureCoverageAttributes, evidenceAttribute, recordEvidence };
|
|
@@ -2,8 +2,8 @@ const require_rolldown_runtime = require('./rolldown-runtime-C_NdSu1c.cjs');
|
|
|
2
2
|
const require_values = require('./values-DApyq53y.cjs');
|
|
3
3
|
const require_tracer_provider = require('./tracer-provider.cjs');
|
|
4
4
|
const require_config = require('./config.cjs');
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const require_init = require('./init-aTZKsAwy.cjs');
|
|
6
|
+
const require_evidence = require('./evidence.cjs');
|
|
7
7
|
const require_trace_helpers = require('./trace-helpers.cjs');
|
|
8
8
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
@@ -11,6 +11,36 @@ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
|
11
11
|
let node_url = require("node:url");
|
|
12
12
|
node_url = require_rolldown_runtime.__toESM(node_url, 1);
|
|
13
13
|
|
|
14
|
+
//#region src/is-thenable.ts
|
|
15
|
+
/**
|
|
16
|
+
* Convert anything `await` would wait on into a native Promise, or return
|
|
17
|
+
* `undefined` for a synchronous value.
|
|
18
|
+
*
|
|
19
|
+
* `instanceof Promise` is not enough. ORM query builders (drizzle, knex,
|
|
20
|
+
* Prisma, Sequelize) are lazy thenables that are not native Promises, and a
|
|
21
|
+
* promise created in another realm fails the check too. Treating those as
|
|
22
|
+
* synchronous ends the span before the work runs, which produces a short
|
|
23
|
+
* parent span with its real children orphaned outside it.
|
|
24
|
+
*/
|
|
25
|
+
function promiseFromThenable(value) {
|
|
26
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) return;
|
|
27
|
+
let then;
|
|
28
|
+
try {
|
|
29
|
+
then = value.then;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return Promise.reject(error);
|
|
32
|
+
}
|
|
33
|
+
if (typeof then !== "function") return void 0;
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
try {
|
|
36
|
+
then.call(value, resolve, reject);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
reject(error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
14
44
|
//#region src/variable-name-inference.ts
|
|
15
45
|
/**
|
|
16
46
|
* Variable Name Inference Utility
|
|
@@ -205,25 +235,41 @@ function createDummyCtx() {
|
|
|
205
235
|
getAllBaggage: () => /* @__PURE__ */ new Map()
|
|
206
236
|
};
|
|
207
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Serialize a captured value, and say whether the ceiling cut it. The flag is
|
|
240
|
+
* separate from the text because `…[truncated]` inside the value is only
|
|
241
|
+
* legible to a human reading one span — a backend cannot filter on it.
|
|
242
|
+
*/
|
|
208
243
|
function serializeCapture(value) {
|
|
209
244
|
if (value === void 0) return void 0;
|
|
210
245
|
try {
|
|
211
246
|
const json = require_values.asString(value) ?? JSON.stringify(value);
|
|
212
247
|
if (json === void 0) return void 0;
|
|
213
|
-
return json.length > CAPTURE_MAX_CHARS ?
|
|
248
|
+
return json.length > CAPTURE_MAX_CHARS ? {
|
|
249
|
+
text: `${json.slice(0, CAPTURE_MAX_CHARS)}…[truncated]`,
|
|
250
|
+
truncated: true
|
|
251
|
+
} : {
|
|
252
|
+
text: json,
|
|
253
|
+
truncated: false
|
|
254
|
+
};
|
|
214
255
|
} catch {
|
|
215
256
|
return;
|
|
216
257
|
}
|
|
217
258
|
}
|
|
218
|
-
function
|
|
259
|
+
function captureAttrs(value, valueAttr, field, enabled) {
|
|
219
260
|
if (!enabled) return {};
|
|
220
|
-
const serialized = serializeCapture(
|
|
221
|
-
|
|
261
|
+
const serialized = serializeCapture(value);
|
|
262
|
+
if (serialized === void 0) return {};
|
|
263
|
+
return {
|
|
264
|
+
[valueAttr]: serialized.text,
|
|
265
|
+
...serialized.truncated && { [require_evidence.evidenceAttribute(field)]: "truncated" }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function captureInputAttrs(args, enabled) {
|
|
269
|
+
return captureAttrs(args.length === 1 ? args[0] : args, AUTOTEL_INPUT_ATTR, "input", enabled);
|
|
222
270
|
}
|
|
223
271
|
function captureOutputAttrs(result, enabled) {
|
|
224
|
-
|
|
225
|
-
const serialized = serializeCapture(result);
|
|
226
|
-
return serialized === void 0 ? {} : { [AUTOTEL_OUTPUT_ATTR]: serialized };
|
|
272
|
+
return captureAttrs(result, AUTOTEL_OUTPUT_ATTR, "output", enabled);
|
|
227
273
|
}
|
|
228
274
|
function truncateErrorMessage(message) {
|
|
229
275
|
return message.length <= 500 ? message : `${message.slice(0, 500)}... (truncated)`;
|
|
@@ -302,7 +348,7 @@ function runWithTraceContextStorage(fn) {
|
|
|
302
348
|
}
|
|
303
349
|
function wrapWithTracingSync(fnFactory, options, variableName) {
|
|
304
350
|
const { tracer, meter } = require_config.getConfig();
|
|
305
|
-
const sampler = options.sampler || new
|
|
351
|
+
const sampler = options.sampler || new require_init.AlwaysSampler();
|
|
306
352
|
const spanName = getSpanName(options, fnFactory, variableName);
|
|
307
353
|
const callCounter = options.withMetrics ? meter.createCounter(`${spanName}.calls`, {
|
|
308
354
|
description: `Call count for ${spanName}`,
|
|
@@ -321,7 +367,10 @@ function wrapWithTracingSync(fnFactory, options, variableName) {
|
|
|
321
367
|
const shouldSample = sampler.shouldSample(samplingContext);
|
|
322
368
|
const sampleRate = sampler.sampleRate?.(samplingContext);
|
|
323
369
|
const needsTailSampling = sampler.needsTailSampling?.() ?? false;
|
|
324
|
-
if (!shouldSample && !needsTailSampling)
|
|
370
|
+
if (!shouldSample && !needsTailSampling) {
|
|
371
|
+
const unsampled = fnFactory(createDummyCtx()).call(this, ...args);
|
|
372
|
+
return promiseFromThenable(unsampled) ?? unsampled;
|
|
373
|
+
}
|
|
325
374
|
const startTime = performance.now();
|
|
326
375
|
const flushRootTelemetry = createRootTelemetryFlusher(options, options.startNewRoot || _opentelemetry_api.trace.getActiveSpan() === void 0);
|
|
327
376
|
const spanOptions = {};
|
|
@@ -330,21 +379,21 @@ function wrapWithTracingSync(fnFactory, options, variableName) {
|
|
|
330
379
|
return tracer.startActiveSpan(spanName, spanOptions, require_init.getActiveContextWithBaggage(), (span) => require_init.runInOperationContext(spanName, () => runWithTraceContextStorage(() => {
|
|
331
380
|
let shouldKeepSpan = true;
|
|
332
381
|
require_trace_helpers.setSpanName(span, spanName);
|
|
333
|
-
if (sampleRate !== void 0 && sampleRate > 1) span.setAttribute(
|
|
382
|
+
if (sampleRate !== void 0 && sampleRate > 1) span.setAttribute(require_init.AUTOTEL_SAMPLING_RATE, sampleRate);
|
|
334
383
|
const fn = fnFactory(require_init.createTraceContext(span));
|
|
335
384
|
const argsAttributes = {
|
|
336
385
|
...captureInputAttrs(args, options.captureInput),
|
|
337
386
|
...options.attributesFromArgs?.(args)
|
|
338
387
|
};
|
|
339
388
|
const handleTailSampling = (success, duration, error) => {
|
|
340
|
-
if (needsTailSampling && sampler.shouldKeepTrace) {
|
|
389
|
+
if (needsTailSampling && sampler.shouldKeepTrace && !require_init.isForceKept(span) && !require_init.debugCaptureRequested()) {
|
|
341
390
|
shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {
|
|
342
391
|
success,
|
|
343
392
|
duration,
|
|
344
393
|
error
|
|
345
394
|
});
|
|
346
|
-
span.setAttribute(
|
|
347
|
-
span.setAttribute(
|
|
395
|
+
span.setAttribute(require_init.AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
|
|
396
|
+
span.setAttribute(require_init.AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
|
|
348
397
|
}
|
|
349
398
|
};
|
|
350
399
|
const onSuccess = (result) => {
|
|
@@ -392,7 +441,8 @@ function wrapWithTracingSync(fnFactory, options, variableName) {
|
|
|
392
441
|
status: "started"
|
|
393
442
|
});
|
|
394
443
|
const result = _opentelemetry_api.context.with(_opentelemetry_api.trace.setSpan(require_init.getActiveContextWithBaggage(), span), () => fn.call(this, ...args));
|
|
395
|
-
|
|
444
|
+
const resultPromise = promiseFromThenable(result);
|
|
445
|
+
if (resultPromise) return resultPromise.then(async (value) => {
|
|
396
446
|
const completed = onSuccess(value);
|
|
397
447
|
await flushRootTelemetry();
|
|
398
448
|
return completed;
|
|
@@ -642,7 +692,8 @@ function span(nameOrOptions, fn) {
|
|
|
642
692
|
try {
|
|
643
693
|
if (attributes) for (const [key, value] of Object.entries(attributes)) span.setAttribute(key, value);
|
|
644
694
|
const result = fn(span);
|
|
645
|
-
|
|
695
|
+
const resultPromise = promiseFromThenable(result);
|
|
696
|
+
if (resultPromise) return resultPromise.then((resolved) => {
|
|
646
697
|
span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
|
|
647
698
|
span.end();
|
|
648
699
|
return resolved;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as TraceContext } from "./trace-context-C1Cn1q0y.cjs";
|
|
2
|
-
import {
|
|
2
|
+
import { m as Sampler } from "./sampling-Er217Mge.cjs";
|
|
3
3
|
import { Attributes, Span, SpanKind } from "@opentelemetry/api";
|
|
4
4
|
//#region src/functional-wrapper.d.ts
|
|
5
5
|
type WrappedFunction<TArgs extends unknown[], TReturn> = (...args: TArgs) => TReturn | Promise<TReturn>;
|
|
@@ -192,7 +192,9 @@ declare const ctx: TraceContext;
|
|
|
192
192
|
* })
|
|
193
193
|
* ```
|
|
194
194
|
*/
|
|
195
|
+
declare function traceImpl<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => PromiseLike<TReturn>): (...args: TArgs) => Promise<TReturn>;
|
|
195
196
|
declare function traceImpl<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
|
|
197
|
+
declare function traceImpl<TArgs extends unknown[], TReturn>(name: string, fn: (...args: TArgs) => PromiseLike<TReturn>): (...args: TArgs) => Promise<TReturn>;
|
|
196
198
|
declare function traceImpl<TArgs extends unknown[], TReturn>(name: string, fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
|
|
197
199
|
declare function traceImpl<TArgs extends unknown[], TReturn>(options: TracingOptions<TArgs, TReturn>, fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn;
|
|
198
200
|
declare function traceImpl(name: string): <TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn) => (...args: TArgs) => TReturn;
|
|
@@ -258,7 +260,7 @@ declare const trace$1: typeof traceImpl & {
|
|
|
258
260
|
* )(ctx => async (data) => { })
|
|
259
261
|
* ```
|
|
260
262
|
*/
|
|
261
|
-
declare function withTracing<TCfgArgs extends unknown[] = unknown[], TCfgReturn = unknown>(options?: TracingOptions<TCfgArgs, TCfgReturn>): <TArgs extends TCfgArgs, TReturn extends TCfgReturn>(fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn |
|
|
263
|
+
declare function withTracing<TCfgArgs extends unknown[] = unknown[], TCfgReturn = unknown>(options?: TracingOptions<TCfgArgs, TCfgReturn>): <TArgs extends TCfgArgs, TReturn extends TCfgReturn>(fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn | PromiseLike<TReturn>) => WrappedFunction<TArgs, TReturn>;
|
|
262
264
|
/**
|
|
263
265
|
* Approach 3: instrument() - Batch auto-instrumentation
|
|
264
266
|
*
|
|
@@ -309,8 +311,13 @@ declare function withTracing<TCfgArgs extends unknown[] = unknown[], TCfgReturn
|
|
|
309
311
|
* })
|
|
310
312
|
* ```
|
|
311
313
|
*/
|
|
314
|
+
type InstrumentedFunction<TFunction extends AnyInstrumentable> = TFunction extends ((...args: infer TArgs) => infer TReturn) ? (...args: TArgs) => TReturn extends PromiseLike<infer TValue> ? Promise<TValue> : TReturn : never;
|
|
315
|
+
type InstrumentedFunctions<T extends Record<string, AnyInstrumentable>> = { [TKey in keyof T]: InstrumentedFunction<T[TKey]>; };
|
|
316
|
+
declare function instrument<TFunction extends AnyInstrumentable>(options: SingleInstrumentOptions<TFunction> & {
|
|
317
|
+
fn: TFunction & ((...args: Parameters<TFunction>) => PromiseLike<unknown>);
|
|
318
|
+
}): (...args: Parameters<TFunction>) => Promise<Awaited<ReturnType<TFunction>>>;
|
|
312
319
|
declare function instrument<TFunction extends AnyInstrumentable>(options: SingleInstrumentOptions<TFunction>): TFunction;
|
|
313
|
-
declare function instrument<T extends Record<string, AnyInstrumentable>>(options: InstrumentOptions<T>): T
|
|
320
|
+
declare function instrument<T extends Record<string, AnyInstrumentable>>(options: InstrumentOptions<T>): InstrumentedFunctions<T>;
|
|
314
321
|
/**
|
|
315
322
|
* Options for span() function
|
|
316
323
|
*/
|
|
@@ -352,10 +359,10 @@ interface SpanOptions {
|
|
|
352
359
|
* })
|
|
353
360
|
* ```
|
|
354
361
|
*/
|
|
362
|
+
declare function span<T = unknown>(name: string, fn: (span: Span) => PromiseLike<T>): Promise<T>;
|
|
363
|
+
declare function span<T = unknown>(options: SpanOptions, fn: (span: Span) => PromiseLike<T>): Promise<T>;
|
|
355
364
|
declare function span<T = unknown>(name: string, fn: (span: Span) => T): T;
|
|
356
|
-
declare function span<T = unknown>(name: string, fn: (span: Span) => Promise<T>): Promise<T>;
|
|
357
365
|
declare function span<T = unknown>(options: SpanOptions, fn: (span: Span) => T): T;
|
|
358
|
-
declare function span<T = unknown>(options: SpanOptions, fn: (span: Span) => Promise<T>): Promise<T>;
|
|
359
366
|
/**
|
|
360
367
|
* Options for withNewContext() function
|
|
361
368
|
*/
|