@ory/argus 0.7.3 → 0.8.1

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/client.js CHANGED
@@ -6,7 +6,7 @@ const client_1 = require("@ory/client");
6
6
  const logger_js_1 = require("./logger.js");
7
7
  const tracer_js_1 = require("./tracer.js");
8
8
  const config_js_1 = require("./config.js");
9
- const otlp_http_js_1 = require("./otel/otlp-http.js");
9
+ const otlp_js_1 = require("./otel/otlp.js");
10
10
  class OryAgentClient {
11
11
  frontend;
12
12
  oauth2;
@@ -642,7 +642,7 @@ class OryAgentClient {
642
642
  exports.OryAgentClient = OryAgentClient;
643
643
  function attachOtlpExporterFromEnv(client, harness) {
644
644
  const meta = client.tracer.processMetadata;
645
- const exporter = (0, otlp_http_js_1.otlpExporterFromEnv)({
645
+ const exporter = (0, otlp_js_1.otlpExporterFromEnv)({
646
646
  harness,
647
647
  hostname: meta.hostname,
648
648
  user: meta.user,
@@ -658,10 +658,8 @@ function attachOtlpExporterFromEnv(client, harness) {
658
658
  return;
659
659
  client.tracer.setExporter(exporter);
660
660
  client.logger.info("otel.export.enabled", {
661
- endpoint: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
662
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
663
- process.env.ORY_OTLP_ENDPOINT,
664
- serviceName: process.env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${harness}`,
661
+ endpoint: exporter.endpoint,
662
+ serviceName: exporter.resource.attributes["service.name"],
665
663
  });
666
664
  }
667
665
  /**
package/dist/dev.js CHANGED
@@ -322,16 +322,7 @@ async function runDevLauncher(config) {
322
322
  ? buildLocalOryEnv(localOry.gatewayUrl, localOry.seed)
323
323
  : { ORY_AGENT_SUBJECT_ID: sessionName }),
324
324
  ...(otelEndpoint
325
- ? {
326
- OTEL_EXPORTER_OTLP_ENDPOINT: otelEndpoint,
327
- // ORY-prefixed alias for harnesses (e.g. Claude Code) that
328
- // filter the env passed to hook subprocesses and drop
329
- // OTEL_*-prefixed vars. The OTLP exporter reads this as a
330
- // fallback so traces still reach Jaeger when OTEL_* is stripped.
331
- ORY_OTLP_ENDPOINT: otelEndpoint,
332
- OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME ??
333
- `ory-agent-plugin-${config.harnessName}`,
334
- }
325
+ ? buildOtelEnv(otelEndpoint, config.harnessName)
335
326
  : {}),
336
327
  };
337
328
  const result = (0, node_child_process_1.spawnSync)(config.command, forwardedArgs, {
@@ -488,10 +479,53 @@ async function bootstrapLocalOry(localDir) {
488
479
  `${seed.permissions.tuples} permissions in '${namespace}' for ${seed.permissions.subject}.`);
489
480
  return { active: true, gatewayUrl, seed };
490
481
  }
482
+ /**
483
+ * OTel env vars the dev launcher propagates into the harness child. Each
484
+ * entry pairs the standard `OTEL_*` name with its `ORY_OTLP_*` alias; the
485
+ * alias is set in lockstep so harnesses (notably Claude Code) that sanitize
486
+ * `OTEL_*` out of the hook-subprocess env still receive the configuration.
487
+ *
488
+ * Keep this list in sync with `ORY_ENV_ALIAS` in
489
+ * `packages/core/src/otel/otlp.ts` — the exporter consults the same aliases
490
+ * on the read side.
491
+ */
492
+ const OTEL_FORWARDED_VARS = [
493
+ ["OTEL_EXPORTER_OTLP_ENDPOINT", "ORY_OTLP_ENDPOINT"],
494
+ ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "ORY_OTLP_TRACES_ENDPOINT"],
495
+ ["OTEL_EXPORTER_OTLP_HEADERS", "ORY_OTLP_HEADERS"],
496
+ ["OTEL_EXPORTER_OTLP_TRACES_HEADERS", "ORY_OTLP_TRACES_HEADERS"],
497
+ ["OTEL_EXPORTER_OTLP_PROTOCOL", "ORY_OTLP_PROTOCOL"],
498
+ ["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "ORY_OTLP_TRACES_PROTOCOL"],
499
+ ["OTEL_SERVICE_NAME", "ORY_OTLP_SERVICE_NAME"],
500
+ ["OTEL_RESOURCE_ATTRIBUTES", "ORY_OTLP_RESOURCE_ATTRIBUTES"],
501
+ ];
502
+ /**
503
+ * Compose the OTLP env block for the harness child process. The resolved
504
+ * endpoint is always written under both `OTEL_EXPORTER_OTLP_ENDPOINT` and
505
+ * `ORY_OTLP_ENDPOINT`; every other OTel knob inherited from the parent
506
+ * shell (and `OTEL_SERVICE_NAME`, which we default when unset) is mirrored
507
+ * to its `ORY_OTLP_*` alias on the way through.
508
+ */
509
+ function buildOtelEnv(endpoint, harnessName) {
510
+ const out = {};
511
+ const defaults = {
512
+ OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
513
+ OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${harnessName}`,
514
+ };
515
+ for (const [standardName, aliasName] of OTEL_FORWARDED_VARS) {
516
+ const value = defaults[standardName] ?? process.env[standardName];
517
+ if (!value)
518
+ continue;
519
+ out[standardName] = value;
520
+ out[aliasName] = value;
521
+ }
522
+ return out;
523
+ }
491
524
  /**
492
525
  * Determine the OTLP endpoint for the dev launch:
493
526
  * 1. Honor OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
494
- * from the parent shell (Honeycomb, custom collector, etc.).
527
+ * (or their ORY_OTLP_* aliases) from the parent shell (Honeycomb,
528
+ * custom collector, etc.).
495
529
  * 2. Otherwise auto-launch a local Jaeger container and use it. If Jaeger
496
530
  * is already reachable (from `local up` or a prior dev launch), reuse
497
531
  * it without spawning another container.
@@ -500,7 +534,9 @@ async function bootstrapLocalOry(localDir) {
500
534
  */
501
535
  async function resolveOtelEndpoint() {
502
536
  const fromEnv = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
503
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
537
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
538
+ process.env.ORY_OTLP_TRACES_ENDPOINT ??
539
+ process.env.ORY_OTLP_ENDPOINT;
504
540
  if (fromEnv)
505
541
  return fromEnv;
506
542
  console.log("[ory-dev] Ensuring Jaeger is running for trace viewing...");
package/dist/index.d.ts CHANGED
@@ -21,4 +21,4 @@ export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkM
21
21
  export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
22
22
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
23
23
  export { summarizeToolInput, summarizeToolOutput, type ToolInputSummary, type ToolOutputSummary, } from "./tool-metadata.js";
24
- export { OtlpHttpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpHttpExporterOptions, } from "./otel/index.js";
24
+ export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpExporterOptions, type OtlpProtocol, } from "./otel/index.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.printOryConfig = exports.runAgentCommand = exports.runConfigureCommand = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = exports.clearSubAgentDynamicCredentials = exports.saveSubAgentDynamicCredentials = exports.loadSubAgentDynamicCredentials = exports.clearAgentDynamicCredentials = exports.saveAgentDynamicCredentials = exports.loadAgentDynamicCredentials = exports.registerAgentClient = exports.fetchClientCredentialsToken = exports.ensureSubAgentIdentity = exports.ensureAgentIdentity = exports.resolveAgentCredentials = exports.ensureAuthenticated = exports.ensureUserAuthenticated = exports.TOKEN_EXPIRY_SKEW_SEC = exports.waitForPeerTokensSync = exports.waitForPeerTokens = exports.clearPkceFlightLock = exports.tryAcquirePkceFlightLock = exports.refreshAndSave = exports.isExpired = exports.clearTokens = exports.saveTokens = exports.loadTokens = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.LOOPBACK_PORTS = exports.buildAuthorizeUrl = exports.sha256Base64Url = exports.generateCodeVerifier = exports.detectHeadless = exports.refreshAccessToken = exports.pkceLogin = exports.getHarnessDataDir = exports.getDataDir = exports.getConfigPath = exports.mutateConfig = exports.resolveConfig = exports.saveConfig = exports.loadConfig = exports.watchTraceFile = exports.formatSpan = exports.deriveTraceId = exports.ActiveSpan = exports.Tracer = exports.redactLogData = exports.DebugLogger = exports.OryAgentClient = void 0;
4
4
  exports.runLocalCommand = exports.ORY_COMMAND_SLUGS = exports.ORY_COMMAND_SKILL_NAMES = exports.ORY_SKILL_NAMES = exports.removeSkillDirs = exports.writeSkillTree = exports.toSkillMarkdown = exports.commandToPlainMarkdown = exports.commandToFrontmatterMarkdown = exports.commandToToml = exports.commandToSkill = exports.renderOryCommands = exports.renderOrySkills = exports.runDevLauncher = exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = exports.printNextSteps = exports.printSetupHelp = exports.removeFlatHooks = exports.mergeFlatHooks = exports.flatHookEntry = exports.removeMatcherHooks = exports.mergeMatcherHooks = exports.matcherHookEntry = exports.resolveHookCommand = exports.isOryHookCommand = exports.writeJsonFile = exports.readJsonFile = exports.parseSetupArgs = exports.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = void 0;
5
- exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpHttpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
5
+ exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
6
6
  var client_js_1 = require("./client.js");
7
7
  Object.defineProperty(exports, "OryAgentClient", { enumerable: true, get: function () { return client_js_1.OryAgentClient; } });
8
8
  var logger_js_1 = require("./logger.js");
@@ -150,6 +150,6 @@ var tool_metadata_js_1 = require("./tool-metadata.js");
150
150
  Object.defineProperty(exports, "summarizeToolInput", { enumerable: true, get: function () { return tool_metadata_js_1.summarizeToolInput; } });
151
151
  Object.defineProperty(exports, "summarizeToolOutput", { enumerable: true, get: function () { return tool_metadata_js_1.summarizeToolOutput; } });
152
152
  var index_js_3 = require("./otel/index.js");
153
- Object.defineProperty(exports, "OtlpHttpExporter", { enumerable: true, get: function () { return index_js_3.OtlpHttpExporter; } });
153
+ Object.defineProperty(exports, "OtlpExporter", { enumerable: true, get: function () { return index_js_3.OtlpExporter; } });
154
154
  Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return index_js_3.otlpExporterFromEnv; } });
155
155
  Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return index_js_3.parseKeyValueList; } });
@@ -1,2 +1,2 @@
1
1
  export type { SpanExporter } from "./exporter.js";
2
- export { OtlpHttpExporter, otlpExporterFromEnv, buildExportRequest, parseKeyValueList, type OtlpHttpExporterOptions, } from "./otlp-http.js";
2
+ export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, toReadableSpan, type OtlpExporterOptions, type OtlpProtocol, } from "./otlp.js";
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseKeyValueList = exports.buildExportRequest = exports.otlpExporterFromEnv = exports.OtlpHttpExporter = void 0;
4
- var otlp_http_js_1 = require("./otlp-http.js");
5
- Object.defineProperty(exports, "OtlpHttpExporter", { enumerable: true, get: function () { return otlp_http_js_1.OtlpHttpExporter; } });
6
- Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return otlp_http_js_1.otlpExporterFromEnv; } });
7
- Object.defineProperty(exports, "buildExportRequest", { enumerable: true, get: function () { return otlp_http_js_1.buildExportRequest; } });
8
- Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return otlp_http_js_1.parseKeyValueList; } });
3
+ exports.toReadableSpan = exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = void 0;
4
+ var otlp_js_1 = require("./otlp.js");
5
+ Object.defineProperty(exports, "OtlpExporter", { enumerable: true, get: function () { return otlp_js_1.OtlpExporter; } });
6
+ Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return otlp_js_1.otlpExporterFromEnv; } });
7
+ Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return otlp_js_1.parseKeyValueList; } });
8
+ Object.defineProperty(exports, "toReadableSpan", { enumerable: true, get: function () { return otlp_js_1.toReadableSpan; } });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * OTLP exporter — SDK-backed.
3
+ *
4
+ * Bridges our internal `TraceSpan` records into the official OpenTelemetry
5
+ * SDK exporters from `@opentelemetry/exporter-trace-otlp-http` and
6
+ * `@opentelemetry/exporter-trace-otlp-proto`. Each completed `TraceSpan`
7
+ * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
8
+ * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
9
+ *
10
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`. Each one has
11
+ * an `ORY_OTLP_*`-prefixed alias that is consulted as a fallback when the
12
+ * standard name is unset. Some harnesses (notably Claude Code) sanitize the
13
+ * env passed to hook subprocesses and drop every `OTEL_*` var, so the dev
14
+ * launcher mirrors each setting under the Ory prefix to survive the strip.
15
+ *
16
+ * Standard name Ory-prefixed fallback
17
+ * ───────────────────────────────────── ─────────────────────────────────
18
+ * OTEL_EXPORTER_OTLP_ENDPOINT ORY_OTLP_ENDPOINT
19
+ * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ORY_OTLP_TRACES_ENDPOINT
20
+ * OTEL_EXPORTER_OTLP_HEADERS ORY_OTLP_HEADERS
21
+ * OTEL_EXPORTER_OTLP_TRACES_HEADERS ORY_OTLP_TRACES_HEADERS
22
+ * OTEL_EXPORTER_OTLP_PROTOCOL ORY_OTLP_PROTOCOL
23
+ * OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ORY_OTLP_TRACES_PROTOCOL
24
+ * OTEL_SERVICE_NAME ORY_OTLP_SERVICE_NAME
25
+ * OTEL_RESOURCE_ATTRIBUTES ORY_OTLP_RESOURCE_ATTRIBUTES
26
+ *
27
+ * Failures are swallowed (callers receive a resolved Promise) so trace
28
+ * export can never break the agent session. Transport errors surface
29
+ * through the `onError` callback.
30
+ */
31
+ import { type AttributeValue } from "@opentelemetry/api";
32
+ import { type Resource } from "@opentelemetry/resources";
33
+ import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
34
+ import type { SpanExporter as SdkSpanExporter } from "@opentelemetry/sdk-trace-base";
35
+ import type { SpanExporter } from "./exporter.js";
36
+ import type { TraceSpan } from "../tracer.js";
37
+ export type OtlpProtocol = "http/json" | "http/protobuf";
38
+ export interface OtlpExporterOptions {
39
+ /** Full traces endpoint (e.g. `http://localhost:4318/v1/traces`). */
40
+ endpoint: string;
41
+ /** Wire protocol. Default: `http/protobuf` (the OTel spec default). */
42
+ protocol?: OtlpProtocol;
43
+ /** Additional HTTP headers to send with each request. */
44
+ headers?: Record<string, string>;
45
+ /** Resource attributes merged into every export. */
46
+ resource?: Record<string, AttributeValue>;
47
+ /** Per-request timeout in milliseconds. Default: 5000. */
48
+ timeoutMs?: number;
49
+ /** Optional sink for transport errors. Defaults to a no-op. */
50
+ onError?: (err: unknown) => void;
51
+ /**
52
+ * Inject a custom SDK exporter (used by tests). When provided, supersedes
53
+ * the protocol selector — the wrapper passes converted ReadableSpans to
54
+ * this exporter as-is.
55
+ */
56
+ sdkExporter?: SdkSpanExporter;
57
+ }
58
+ /**
59
+ * OTLP exporter that delegates wire-format serialization and HTTP transport
60
+ * to the official `@opentelemetry/exporter-trace-otlp-*` SDK exporters.
61
+ */
62
+ export declare class OtlpExporter implements SpanExporter {
63
+ readonly endpoint: string;
64
+ readonly protocol: OtlpProtocol;
65
+ readonly headers: Record<string, string>;
66
+ readonly resource: Resource;
67
+ private readonly delegate;
68
+ private readonly onError;
69
+ private readonly inFlight;
70
+ constructor(opts: OtlpExporterOptions);
71
+ export(spans: ReadonlyArray<TraceSpan>): Promise<void>;
72
+ shutdown(): Promise<void>;
73
+ }
74
+ /**
75
+ * Build an OtlpExporter from process env. Returns undefined when no
76
+ * endpoint is configured or the requested protocol is unsupported.
77
+ */
78
+ export declare function otlpExporterFromEnv(opts: {
79
+ harness: string;
80
+ hostname?: string;
81
+ user?: string;
82
+ pluginVersion?: string;
83
+ cwd?: string;
84
+ gitBranch?: string;
85
+ gitCommit?: string;
86
+ onError?: (err: unknown) => void;
87
+ env?: NodeJS.ProcessEnv;
88
+ }): OtlpExporter | undefined;
89
+ /**
90
+ * Read an OTel env var, falling back to its `ORY_OTLP_*`-prefixed alias when
91
+ * the standard name is unset. Trims whitespace and treats empty strings as
92
+ * absent so a propagated-but-blank var doesn't shadow the alias.
93
+ */
94
+ export declare function readEnvWithAlias(env: NodeJS.ProcessEnv, standardName: string): string | undefined;
95
+ /**
96
+ * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
97
+ */
98
+ export declare function parseKeyValueList(raw: string | undefined): Record<string, string>;
99
+ /**
100
+ * Convert our internal `TraceSpan` record to an OTel SDK `ReadableSpan`.
101
+ * Exported for tests; the OTLP exporters consume this shape directly.
102
+ */
103
+ export declare function toReadableSpan(span: TraceSpan, resource: Resource): ReadableSpan;
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ /**
3
+ * OTLP exporter — SDK-backed.
4
+ *
5
+ * Bridges our internal `TraceSpan` records into the official OpenTelemetry
6
+ * SDK exporters from `@opentelemetry/exporter-trace-otlp-http` and
7
+ * `@opentelemetry/exporter-trace-otlp-proto`. Each completed `TraceSpan`
8
+ * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
9
+ * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
10
+ *
11
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`. Each one has
12
+ * an `ORY_OTLP_*`-prefixed alias that is consulted as a fallback when the
13
+ * standard name is unset. Some harnesses (notably Claude Code) sanitize the
14
+ * env passed to hook subprocesses and drop every `OTEL_*` var, so the dev
15
+ * launcher mirrors each setting under the Ory prefix to survive the strip.
16
+ *
17
+ * Standard name Ory-prefixed fallback
18
+ * ───────────────────────────────────── ─────────────────────────────────
19
+ * OTEL_EXPORTER_OTLP_ENDPOINT ORY_OTLP_ENDPOINT
20
+ * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ORY_OTLP_TRACES_ENDPOINT
21
+ * OTEL_EXPORTER_OTLP_HEADERS ORY_OTLP_HEADERS
22
+ * OTEL_EXPORTER_OTLP_TRACES_HEADERS ORY_OTLP_TRACES_HEADERS
23
+ * OTEL_EXPORTER_OTLP_PROTOCOL ORY_OTLP_PROTOCOL
24
+ * OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ORY_OTLP_TRACES_PROTOCOL
25
+ * OTEL_SERVICE_NAME ORY_OTLP_SERVICE_NAME
26
+ * OTEL_RESOURCE_ATTRIBUTES ORY_OTLP_RESOURCE_ATTRIBUTES
27
+ *
28
+ * Failures are swallowed (callers receive a resolved Promise) so trace
29
+ * export can never break the agent session. Transport errors surface
30
+ * through the `onError` callback.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.OtlpExporter = void 0;
34
+ exports.otlpExporterFromEnv = otlpExporterFromEnv;
35
+ exports.readEnvWithAlias = readEnvWithAlias;
36
+ exports.parseKeyValueList = parseKeyValueList;
37
+ exports.toReadableSpan = toReadableSpan;
38
+ const api_1 = require("@opentelemetry/api");
39
+ const core_1 = require("@opentelemetry/core");
40
+ const resources_1 = require("@opentelemetry/resources");
41
+ const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
42
+ const exporter_trace_otlp_proto_1 = require("@opentelemetry/exporter-trace-otlp-proto");
43
+ const TRACES_PATH = "/v1/traces";
44
+ const SCOPE = {
45
+ name: "ory-agent-plugins",
46
+ };
47
+ /**
48
+ * OTLP exporter that delegates wire-format serialization and HTTP transport
49
+ * to the official `@opentelemetry/exporter-trace-otlp-*` SDK exporters.
50
+ */
51
+ class OtlpExporter {
52
+ endpoint;
53
+ protocol;
54
+ headers;
55
+ resource;
56
+ delegate;
57
+ onError;
58
+ inFlight = new Set();
59
+ constructor(opts) {
60
+ this.endpoint = opts.endpoint;
61
+ this.protocol = opts.protocol ?? "http/protobuf";
62
+ this.headers = { ...(opts.headers ?? {}) };
63
+ this.resource = (0, resources_1.resourceFromAttributes)(stripUndefined(opts.resource ?? {}));
64
+ this.onError = opts.onError ?? (() => undefined);
65
+ this.delegate =
66
+ opts.sdkExporter ??
67
+ buildSdkExporter(this.endpoint, this.protocol, this.headers, opts.timeoutMs);
68
+ }
69
+ export(spans) {
70
+ if (spans.length === 0)
71
+ return Promise.resolve();
72
+ const readables = spans.map((s) => toReadableSpan(s, this.resource));
73
+ const promise = new Promise((resolve) => {
74
+ try {
75
+ this.delegate.export(readables, (result) => {
76
+ if (result.code !== core_1.ExportResultCode.SUCCESS) {
77
+ this.onError(result.error ?? new Error("OTLP export failed"));
78
+ }
79
+ resolve();
80
+ });
81
+ }
82
+ catch (err) {
83
+ // Defensive — SDK exporter contract says export() must not throw,
84
+ // but we honor our own contract regardless.
85
+ this.onError(err);
86
+ resolve();
87
+ }
88
+ });
89
+ this.inFlight.add(promise);
90
+ promise.finally(() => this.inFlight.delete(promise));
91
+ return promise;
92
+ }
93
+ async shutdown() {
94
+ // Drain in-flight exports first. Subprocess hooks (claude-code, codex,
95
+ // gemini-cli) call process.exit() the instant they hand back stdout —
96
+ // without this drain the underlying HTTP request is killed mid-flight
97
+ // and spans never reach the collector even though the NDJSON file
98
+ // already contains them.
99
+ if (this.inFlight.size > 0) {
100
+ await Promise.allSettled([...this.inFlight]);
101
+ }
102
+ try {
103
+ await this.delegate.shutdown();
104
+ }
105
+ catch (err) {
106
+ this.onError(err);
107
+ }
108
+ }
109
+ }
110
+ exports.OtlpExporter = OtlpExporter;
111
+ function buildSdkExporter(endpoint, protocol, headers, timeoutMs) {
112
+ const config = {
113
+ url: endpoint,
114
+ headers,
115
+ timeoutMillis: timeoutMs ?? 5_000,
116
+ };
117
+ return protocol === "http/json"
118
+ ? new exporter_trace_otlp_http_1.OTLPTraceExporter(config)
119
+ : new exporter_trace_otlp_proto_1.OTLPTraceExporter(config);
120
+ }
121
+ // ─── Factory ───────────────────────────────────────────────────────
122
+ /**
123
+ * Build an OtlpExporter from process env. Returns undefined when no
124
+ * endpoint is configured or the requested protocol is unsupported.
125
+ */
126
+ function otlpExporterFromEnv(opts) {
127
+ const env = opts.env ?? process.env;
128
+ const endpoint = resolveEndpoint(env);
129
+ if (!endpoint)
130
+ return undefined;
131
+ const protocol = resolveProtocol(env, opts.onError);
132
+ if (!protocol)
133
+ return undefined;
134
+ const headers = mergeHeaders(parseKeyValueList(readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_HEADERS")), parseKeyValueList(readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_HEADERS")));
135
+ const resource = {
136
+ "service.name": readEnvWithAlias(env, "OTEL_SERVICE_NAME") ??
137
+ `ory-agent-plugin-${opts.harness}`,
138
+ "service.namespace": "ory.agent_plugins",
139
+ "ory.harness": opts.harness,
140
+ "process.pid": process.pid,
141
+ "process.runtime.name": "nodejs",
142
+ "process.runtime.version": process.version,
143
+ };
144
+ if (opts.hostname)
145
+ resource["host.name"] = opts.hostname;
146
+ if (opts.user)
147
+ resource["host.user"] = opts.user;
148
+ if (opts.pluginVersion)
149
+ resource["service.version"] = opts.pluginVersion;
150
+ if (opts.cwd)
151
+ resource["process.cwd"] = opts.cwd;
152
+ if (opts.gitBranch)
153
+ resource["ory.git.branch"] = opts.gitBranch;
154
+ if (opts.gitCommit)
155
+ resource["ory.git.commit"] = opts.gitCommit;
156
+ for (const [k, v] of Object.entries(parseKeyValueList(readEnvWithAlias(env, "OTEL_RESOURCE_ATTRIBUTES")))) {
157
+ resource[k] = v;
158
+ }
159
+ return new OtlpExporter({
160
+ endpoint,
161
+ protocol,
162
+ headers,
163
+ resource,
164
+ onError: opts.onError,
165
+ });
166
+ }
167
+ function resolveEndpoint(env) {
168
+ // ORY_OTLP_*-prefixed aliases survive harnesses (e.g. Claude Code) that
169
+ // sanitize the env passed to hook subprocesses and drop OTEL_*-prefixed
170
+ // vars. The standard OTel name still wins when present.
171
+ const tracesUrl = readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT");
172
+ if (tracesUrl)
173
+ return tracesUrl;
174
+ const base = readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_ENDPOINT");
175
+ if (!base)
176
+ return undefined;
177
+ return base.replace(/\/+$/, "") + TRACES_PATH;
178
+ }
179
+ function resolveProtocol(env, onError) {
180
+ const raw = (readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") ??
181
+ readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_PROTOCOL") ??
182
+ "http/protobuf")
183
+ .trim()
184
+ .toLowerCase();
185
+ if (raw === "http/protobuf" || raw === "http/json")
186
+ return raw;
187
+ onError?.(new Error(`OTEL_EXPORTER_OTLP_PROTOCOL=${raw} not supported (only http/protobuf and http/json). Skipping OTel export.`));
188
+ return undefined;
189
+ }
190
+ /**
191
+ * Standard OTel env var name → Ory-prefixed fallback. Standard wins when set;
192
+ * the alias is consulted only when the standard name is empty/missing. The
193
+ * historical `ORY_OTLP_ENDPOINT` alias for `OTEL_EXPORTER_OTLP_ENDPOINT` is
194
+ * preserved verbatim for compatibility with existing dev-launcher envs.
195
+ */
196
+ const ORY_ENV_ALIAS = {
197
+ OTEL_EXPORTER_OTLP_ENDPOINT: "ORY_OTLP_ENDPOINT",
198
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "ORY_OTLP_TRACES_ENDPOINT",
199
+ OTEL_EXPORTER_OTLP_HEADERS: "ORY_OTLP_HEADERS",
200
+ OTEL_EXPORTER_OTLP_TRACES_HEADERS: "ORY_OTLP_TRACES_HEADERS",
201
+ OTEL_EXPORTER_OTLP_PROTOCOL: "ORY_OTLP_PROTOCOL",
202
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "ORY_OTLP_TRACES_PROTOCOL",
203
+ OTEL_SERVICE_NAME: "ORY_OTLP_SERVICE_NAME",
204
+ OTEL_RESOURCE_ATTRIBUTES: "ORY_OTLP_RESOURCE_ATTRIBUTES",
205
+ };
206
+ /**
207
+ * Read an OTel env var, falling back to its `ORY_OTLP_*`-prefixed alias when
208
+ * the standard name is unset. Trims whitespace and treats empty strings as
209
+ * absent so a propagated-but-blank var doesn't shadow the alias.
210
+ */
211
+ function readEnvWithAlias(env, standardName) {
212
+ const primary = env[standardName]?.trim();
213
+ if (primary)
214
+ return primary;
215
+ const aliasName = ORY_ENV_ALIAS[standardName];
216
+ if (!aliasName)
217
+ return undefined;
218
+ const alias = env[aliasName]?.trim();
219
+ return alias || undefined;
220
+ }
221
+ // ─── Env parsing helpers ───────────────────────────────────────────
222
+ /**
223
+ * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
224
+ */
225
+ function parseKeyValueList(raw) {
226
+ const out = {};
227
+ if (!raw)
228
+ return out;
229
+ for (const pair of raw.split(",")) {
230
+ const trimmed = pair.trim();
231
+ if (!trimmed)
232
+ continue;
233
+ const eq = trimmed.indexOf("=");
234
+ if (eq <= 0)
235
+ continue;
236
+ const key = trimmed.slice(0, eq).trim();
237
+ const value = trimmed.slice(eq + 1).trim();
238
+ if (!key)
239
+ continue;
240
+ out[key] = safeDecodeURIComponent(value);
241
+ }
242
+ return out;
243
+ }
244
+ function safeDecodeURIComponent(value) {
245
+ try {
246
+ return decodeURIComponent(value);
247
+ }
248
+ catch {
249
+ return value;
250
+ }
251
+ }
252
+ function mergeHeaders(...sources) {
253
+ const out = {};
254
+ for (const src of sources) {
255
+ for (const [k, v] of Object.entries(src)) {
256
+ out[k.toLowerCase()] = v;
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+ // ─── TraceSpan → ReadableSpan conversion ───────────────────────────
262
+ /**
263
+ * Convert our internal `TraceSpan` record to an OTel SDK `ReadableSpan`.
264
+ * Exported for tests; the OTLP exporters consume this shape directly.
265
+ */
266
+ function toReadableSpan(span, resource) {
267
+ // OTLP wire format expects 32-hex traceId / 16-hex spanId. Our internal
268
+ // IDs are shorter (hashed session prefix + random suffix), so pad with
269
+ // leading zeros to canonical width.
270
+ const traceId = padHex(span.traceId, 32);
271
+ const spanId = padHex(span.spanId, 16);
272
+ const ctx = {
273
+ traceId,
274
+ spanId,
275
+ traceFlags: 1, // sampled
276
+ isRemote: false,
277
+ };
278
+ const parentSpanContext = span.parentSpanId
279
+ ? {
280
+ traceId,
281
+ spanId: padHex(span.parentSpanId, 16),
282
+ traceFlags: 1,
283
+ isRemote: false,
284
+ }
285
+ : undefined;
286
+ const startTime = isoToHrTime(span.startedAt);
287
+ const endTime = isoToHrTime(span.endedAt);
288
+ const attributes = {
289
+ "ory.event": span.event,
290
+ "ory.span.status": span.status,
291
+ "ory.duration_ms": span.durationMs,
292
+ };
293
+ if (span.sessionId)
294
+ attributes["ory.session_id"] = span.sessionId;
295
+ for (const [k, v] of Object.entries(flattenAttributes(span.attributes ?? {}))) {
296
+ attributes[k] = v;
297
+ }
298
+ return {
299
+ name: span.event,
300
+ kind: api_1.SpanKind.INTERNAL,
301
+ spanContext: () => ctx,
302
+ parentSpanContext,
303
+ startTime,
304
+ endTime,
305
+ status: toOtelStatus(span.status),
306
+ attributes,
307
+ links: [],
308
+ events: [],
309
+ duration: hrTimeSubtract(endTime, startTime),
310
+ ended: true,
311
+ resource,
312
+ instrumentationScope: SCOPE,
313
+ droppedAttributesCount: 0,
314
+ droppedEventsCount: 0,
315
+ droppedLinksCount: 0,
316
+ };
317
+ }
318
+ function toOtelStatus(status) {
319
+ switch (status) {
320
+ case "ok":
321
+ return { code: api_1.SpanStatusCode.OK };
322
+ case "error":
323
+ return { code: api_1.SpanStatusCode.ERROR, message: "error" };
324
+ case "denied":
325
+ return { code: api_1.SpanStatusCode.ERROR, message: "denied" };
326
+ case "skipped":
327
+ default:
328
+ return { code: api_1.SpanStatusCode.UNSET };
329
+ }
330
+ }
331
+ function isoToHrTime(iso) {
332
+ const ms = Date.parse(iso);
333
+ if (!Number.isFinite(ms))
334
+ return [0, 0];
335
+ const seconds = Math.trunc(ms / 1000);
336
+ const nanos = (ms - seconds * 1000) * 1_000_000;
337
+ return [seconds, nanos];
338
+ }
339
+ function hrTimeSubtract(a, b) {
340
+ let sec = a[0] - b[0];
341
+ let nanos = a[1] - b[1];
342
+ if (nanos < 0) {
343
+ sec -= 1;
344
+ nanos += 1_000_000_000;
345
+ }
346
+ if (sec < 0) {
347
+ return [0, 0];
348
+ }
349
+ return [sec, nanos];
350
+ }
351
+ function padHex(hex, length) {
352
+ const lower = hex.toLowerCase().replace(/[^0-9a-f]/g, "");
353
+ if (lower.length >= length)
354
+ return lower.slice(-length);
355
+ return lower.padStart(length, "0");
356
+ }
357
+ function flattenAttributes(attrs) {
358
+ const out = {};
359
+ for (const [key, value] of Object.entries(attrs)) {
360
+ if (value === null || value === undefined)
361
+ continue;
362
+ if (typeof value === "string" ||
363
+ typeof value === "number" ||
364
+ typeof value === "boolean") {
365
+ out[key] = value;
366
+ continue;
367
+ }
368
+ try {
369
+ out[key] = JSON.stringify(value);
370
+ }
371
+ catch {
372
+ /* skip un-serializable values */
373
+ }
374
+ }
375
+ return out;
376
+ }
377
+ function stripUndefined(source) {
378
+ const out = {};
379
+ for (const [k, v] of Object.entries(source)) {
380
+ if (v === undefined)
381
+ continue;
382
+ out[k] = v;
383
+ }
384
+ return out;
385
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",
@@ -65,6 +65,12 @@
65
65
  "!dist/**/*.tsbuildinfo"
66
66
  ],
67
67
  "dependencies": {
68
+ "@opentelemetry/api": "^1.9.0",
69
+ "@opentelemetry/core": "^2.8.0",
70
+ "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
71
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0",
72
+ "@opentelemetry/resources": "^2.8.0",
73
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
68
74
  "@ory/client": "^1.22.37",
69
75
  "open": "^11.0.0"
70
76
  },
@@ -1,116 +0,0 @@
1
- /**
2
- * OTLP/HTTP JSON exporter.
3
- *
4
- * Serializes TraceSpan records into the OTLP ExportTraceServiceRequest
5
- * shape and POSTs them to an OTLP HTTP endpoint as JSON. Works with
6
- * Jaeger's OTLP receiver, Grafana Tempo, OpenTelemetry Collector,
7
- * Honeycomb, and any other vendor that accepts OTLP/HTTP.
8
- *
9
- * No protobuf dependency — relies on the global `fetch`. The exporter
10
- * is intentionally side-effect-free: failures are swallowed (callers
11
- * receive a resolved Promise) so trace export can never break the
12
- * agent session.
13
- *
14
- * Standard env vars consumed by `otlpExporterFromEnv()`:
15
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
16
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
17
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
18
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS headers specific to traces
19
- * - OTEL_SERVICE_NAME service.name resource attribute
20
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attrs
21
- */
22
- import type { SpanExporter } from "./exporter.js";
23
- import type { TraceSpan } from "../tracer.js";
24
- export interface OtlpHttpExporterOptions {
25
- /** Full traces endpoint (e.g. `http://localhost:4318/v1/traces`). */
26
- endpoint: string;
27
- /** Additional HTTP headers to send with each request. */
28
- headers?: Record<string, string>;
29
- /** Resource attributes (service.name, host.name, …) merged into every export. */
30
- resource?: Record<string, AttributeValue>;
31
- /** Per-request timeout in milliseconds. Default: 5000. */
32
- timeoutMs?: number;
33
- /** Optional sink for transport errors. Defaults to a no-op. */
34
- onError?: (err: unknown) => void;
35
- /**
36
- * Override the fetch function (used by tests). Defaults to global fetch.
37
- */
38
- fetch?: typeof fetch;
39
- }
40
- export declare class OtlpHttpExporter implements SpanExporter {
41
- private readonly endpoint;
42
- private readonly headers;
43
- private readonly resource;
44
- private readonly timeoutMs;
45
- private readonly onError;
46
- private readonly fetchImpl;
47
- private readonly inFlight;
48
- constructor(opts: OtlpHttpExporterOptions);
49
- export(spans: ReadonlyArray<TraceSpan>): Promise<void>;
50
- private send;
51
- shutdown(): Promise<void>;
52
- }
53
- /**
54
- * Build an OtlpHttpExporter from process env. Returns undefined when no
55
- * endpoint is configured.
56
- */
57
- export declare function otlpExporterFromEnv(opts: {
58
- harness: string;
59
- hostname?: string;
60
- user?: string;
61
- pluginVersion?: string;
62
- cwd?: string;
63
- gitBranch?: string;
64
- gitCommit?: string;
65
- onError?: (err: unknown) => void;
66
- env?: NodeJS.ProcessEnv;
67
- }): OtlpHttpExporter | undefined;
68
- /**
69
- * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
70
- */
71
- export declare function parseKeyValueList(raw: string | undefined): Record<string, string>;
72
- type AttributeValue = string | number | boolean | null | undefined;
73
- interface OtlpKeyValue {
74
- key: string;
75
- value: OtlpAnyValue;
76
- }
77
- interface OtlpAnyValue {
78
- stringValue?: string;
79
- intValue?: string;
80
- doubleValue?: number;
81
- boolValue?: boolean;
82
- arrayValue?: {
83
- values: OtlpAnyValue[];
84
- };
85
- }
86
- interface OtlpStatus {
87
- code: 0 | 1 | 2;
88
- message?: string;
89
- }
90
- interface OtlpSpan {
91
- traceId: string;
92
- spanId: string;
93
- parentSpanId?: string;
94
- name: string;
95
- kind: number;
96
- startTimeUnixNano: string;
97
- endTimeUnixNano: string;
98
- attributes?: OtlpKeyValue[];
99
- status?: OtlpStatus;
100
- }
101
- interface OtlpResourceSpans {
102
- resource: {
103
- attributes: OtlpKeyValue[];
104
- };
105
- scopeSpans: Array<{
106
- scope: {
107
- name: string;
108
- };
109
- spans: OtlpSpan[];
110
- }>;
111
- }
112
- interface OtlpExportRequest {
113
- resourceSpans: OtlpResourceSpans[];
114
- }
115
- export declare function buildExportRequest(spans: ReadonlyArray<TraceSpan>, resource: Record<string, AttributeValue>): OtlpExportRequest;
116
- export {};
@@ -1,322 +0,0 @@
1
- "use strict";
2
- /**
3
- * OTLP/HTTP JSON exporter.
4
- *
5
- * Serializes TraceSpan records into the OTLP ExportTraceServiceRequest
6
- * shape and POSTs them to an OTLP HTTP endpoint as JSON. Works with
7
- * Jaeger's OTLP receiver, Grafana Tempo, OpenTelemetry Collector,
8
- * Honeycomb, and any other vendor that accepts OTLP/HTTP.
9
- *
10
- * No protobuf dependency — relies on the global `fetch`. The exporter
11
- * is intentionally side-effect-free: failures are swallowed (callers
12
- * receive a resolved Promise) so trace export can never break the
13
- * agent session.
14
- *
15
- * Standard env vars consumed by `otlpExporterFromEnv()`:
16
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
17
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
18
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
19
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS headers specific to traces
20
- * - OTEL_SERVICE_NAME service.name resource attribute
21
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attrs
22
- */
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.OtlpHttpExporter = void 0;
25
- exports.otlpExporterFromEnv = otlpExporterFromEnv;
26
- exports.parseKeyValueList = parseKeyValueList;
27
- exports.buildExportRequest = buildExportRequest;
28
- const TRACES_PATH = "/v1/traces";
29
- const SCOPE_NAME = "ory-agent-plugins";
30
- class OtlpHttpExporter {
31
- endpoint;
32
- headers;
33
- resource;
34
- timeoutMs;
35
- onError;
36
- fetchImpl;
37
- inFlight = new Set();
38
- constructor(opts) {
39
- this.endpoint = opts.endpoint;
40
- this.headers = { "content-type": "application/json", ...opts.headers };
41
- this.resource = opts.resource ?? {};
42
- this.timeoutMs = opts.timeoutMs ?? 5_000;
43
- this.onError = opts.onError ?? (() => undefined);
44
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
45
- }
46
- export(spans) {
47
- if (spans.length === 0)
48
- return Promise.resolve();
49
- if (typeof this.fetchImpl !== "function") {
50
- this.onError(new Error("global fetch is not available"));
51
- return Promise.resolve();
52
- }
53
- const promise = this.send(spans);
54
- this.inFlight.add(promise);
55
- promise.finally(() => this.inFlight.delete(promise));
56
- return promise;
57
- }
58
- async send(spans) {
59
- const payload = buildExportRequest(spans, this.resource);
60
- const controller = new AbortController();
61
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
62
- try {
63
- const response = await this.fetchImpl(this.endpoint, {
64
- method: "POST",
65
- headers: this.headers,
66
- body: JSON.stringify(payload),
67
- signal: controller.signal,
68
- });
69
- if (!response.ok) {
70
- // Drain body so the connection can be reused but ignore the value.
71
- try {
72
- await response.text();
73
- }
74
- catch {
75
- /* ignore */
76
- }
77
- this.onError(new Error(`OTLP export failed with status ${response.status}`));
78
- }
79
- }
80
- catch (err) {
81
- this.onError(err);
82
- }
83
- finally {
84
- clearTimeout(timer);
85
- }
86
- }
87
- async shutdown() {
88
- // Drain in-flight exports. Subprocess hooks (claude-code, codex,
89
- // gemini-cli) call process.exit() the instant they hand back stdout —
90
- // without this drain the OTLP fetch is killed mid-flight and spans
91
- // never reach the collector even though the NDJSON file already
92
- // contains them.
93
- if (this.inFlight.size === 0)
94
- return;
95
- await Promise.allSettled([...this.inFlight]);
96
- }
97
- }
98
- exports.OtlpHttpExporter = OtlpHttpExporter;
99
- /**
100
- * Build an OtlpHttpExporter from process env. Returns undefined when no
101
- * endpoint is configured.
102
- */
103
- function otlpExporterFromEnv(opts) {
104
- const env = opts.env ?? process.env;
105
- const endpoint = resolveEndpoint(env);
106
- if (!endpoint)
107
- return undefined;
108
- const protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL;
109
- if (protocol && protocol !== "http/json") {
110
- opts.onError?.(new Error(`OTEL_EXPORTER_OTLP_PROTOCOL=${protocol} not supported (only http/json). Skipping OTel export.`));
111
- return undefined;
112
- }
113
- const headers = mergeHeaders(parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS), parseKeyValueList(env.OTEL_EXPORTER_OTLP_TRACES_HEADERS));
114
- const resource = {
115
- "service.name": env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${opts.harness}`,
116
- "service.namespace": "ory.agent_plugins",
117
- "ory.harness": opts.harness,
118
- "process.pid": process.pid,
119
- "process.runtime.name": "nodejs",
120
- "process.runtime.version": process.version,
121
- };
122
- if (opts.hostname)
123
- resource["host.name"] = opts.hostname;
124
- if (opts.user)
125
- resource["host.user"] = opts.user;
126
- if (opts.pluginVersion) {
127
- resource["service.version"] = opts.pluginVersion;
128
- }
129
- if (opts.cwd)
130
- resource["process.cwd"] = opts.cwd;
131
- if (opts.gitBranch)
132
- resource["ory.git.branch"] = opts.gitBranch;
133
- if (opts.gitCommit)
134
- resource["ory.git.commit"] = opts.gitCommit;
135
- for (const [k, v] of Object.entries(parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES))) {
136
- resource[k] = v;
137
- }
138
- return new OtlpHttpExporter({
139
- endpoint,
140
- headers,
141
- resource,
142
- onError: opts.onError,
143
- });
144
- }
145
- // ─── Endpoint / header parsing ─────────────────────────────────────
146
- function resolveEndpoint(env) {
147
- const tracesUrl = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
148
- if (tracesUrl) {
149
- // User specified the full traces URL — use as-is.
150
- return tracesUrl;
151
- }
152
- // ORY_OTLP_ENDPOINT is an ORY-prefixed alias used by the dev launcher.
153
- // Some harnesses (e.g. Claude Code) filter the env they pass to hook
154
- // subprocesses and drop OTEL_*-prefixed vars, but allow ORY_*-prefixed
155
- // ones through. The alias lets the launcher hand the OTLP endpoint to
156
- // the hook process without depending on OTEL_* propagation.
157
- const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.ORY_OTLP_ENDPOINT?.trim();
158
- if (!base)
159
- return undefined;
160
- return base.replace(/\/+$/, "") + TRACES_PATH;
161
- }
162
- /**
163
- * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
164
- */
165
- function parseKeyValueList(raw) {
166
- const out = {};
167
- if (!raw)
168
- return out;
169
- for (const pair of raw.split(",")) {
170
- const trimmed = pair.trim();
171
- if (!trimmed)
172
- continue;
173
- const eq = trimmed.indexOf("=");
174
- if (eq <= 0)
175
- continue;
176
- const key = trimmed.slice(0, eq).trim();
177
- const value = trimmed.slice(eq + 1).trim();
178
- if (!key)
179
- continue;
180
- out[key] = safeDecodeURIComponent(value);
181
- }
182
- return out;
183
- }
184
- function safeDecodeURIComponent(value) {
185
- try {
186
- return decodeURIComponent(value);
187
- }
188
- catch {
189
- return value;
190
- }
191
- }
192
- function mergeHeaders(...sources) {
193
- const out = {};
194
- for (const src of sources) {
195
- for (const [k, v] of Object.entries(src)) {
196
- out[k.toLowerCase()] = v;
197
- }
198
- }
199
- return out;
200
- }
201
- function buildExportRequest(spans, resource) {
202
- const otlpSpans = spans.map(toOtlpSpan);
203
- return {
204
- resourceSpans: [
205
- {
206
- resource: { attributes: toAttributes(resource) },
207
- scopeSpans: [
208
- {
209
- scope: { name: SCOPE_NAME },
210
- spans: otlpSpans,
211
- },
212
- ],
213
- },
214
- ],
215
- };
216
- }
217
- function toOtlpSpan(span) {
218
- // OTLP/HTTP JSON expects 32-hex traceId and 16-hex spanId per the
219
- // proto3-JSON canonical mapping with hex string encoding (the OTel
220
- // spec also allows base64; modern collectors and Honeycomb accept
221
- // hex). Pad short ids with leading zeros.
222
- const status = toOtlpStatus(span.status);
223
- const baseAttributes = {
224
- "ory.event": span.event,
225
- "ory.span.status": span.status,
226
- "ory.duration_ms": span.durationMs,
227
- };
228
- if (span.sessionId)
229
- baseAttributes["ory.session_id"] = span.sessionId;
230
- const flat = flattenAttributes(span.attributes ?? {});
231
- const attrs = { ...baseAttributes, ...flat };
232
- return {
233
- traceId: padHex(span.traceId, 32),
234
- spanId: padHex(span.spanId, 16),
235
- ...(span.parentSpanId
236
- ? { parentSpanId: padHex(span.parentSpanId, 16) }
237
- : {}),
238
- name: span.event,
239
- kind: 1, // SPAN_KIND_INTERNAL
240
- startTimeUnixNano: isoToUnixNano(span.startedAt),
241
- endTimeUnixNano: isoToUnixNano(span.endedAt),
242
- attributes: toAttributes(attrs),
243
- ...(status ? { status } : {}),
244
- };
245
- }
246
- function toOtlpStatus(status) {
247
- switch (status) {
248
- case "ok":
249
- return { code: 1 };
250
- case "error":
251
- return { code: 2, message: "error" };
252
- case "denied":
253
- return { code: 2, message: "denied" };
254
- case "skipped":
255
- return undefined; // STATUS_CODE_UNSET
256
- default:
257
- return undefined;
258
- }
259
- }
260
- function isoToUnixNano(iso) {
261
- const ms = Date.parse(iso);
262
- if (!Number.isFinite(ms))
263
- return "0";
264
- // BigInt avoids 2^53 rounding for ns timestamps.
265
- return (BigInt(ms) * 1000000n).toString();
266
- }
267
- function padHex(hex, length) {
268
- const lower = hex.toLowerCase().replace(/[^0-9a-f]/g, "");
269
- if (lower.length >= length)
270
- return lower.slice(-length);
271
- return lower.padStart(length, "0");
272
- }
273
- function toAttributes(source) {
274
- const out = [];
275
- for (const [key, value] of Object.entries(source)) {
276
- const v = toAnyValue(value);
277
- if (v)
278
- out.push({ key, value: v });
279
- }
280
- return out;
281
- }
282
- function toAnyValue(value) {
283
- if (value === null || value === undefined)
284
- return undefined;
285
- if (typeof value === "string")
286
- return { stringValue: value };
287
- if (typeof value === "boolean")
288
- return { boolValue: value };
289
- if (typeof value === "number") {
290
- if (Number.isInteger(value)) {
291
- return { intValue: value.toString() };
292
- }
293
- return { doubleValue: value };
294
- }
295
- return undefined;
296
- }
297
- /**
298
- * Flatten nested attribute objects into a single-level Record where
299
- * complex values are JSON-stringified. Keeps OTLP attributes compact
300
- * and queryable as scalars in vendor backends.
301
- */
302
- function flattenAttributes(attrs) {
303
- const out = {};
304
- for (const [key, value] of Object.entries(attrs)) {
305
- if (value === null || value === undefined)
306
- continue;
307
- if (typeof value === "string" ||
308
- typeof value === "number" ||
309
- typeof value === "boolean") {
310
- out[key] = value;
311
- }
312
- else {
313
- try {
314
- out[key] = JSON.stringify(value);
315
- }
316
- catch {
317
- /* skip un-serializable values */
318
- }
319
- }
320
- }
321
- return out;
322
- }