@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 +4 -6
- package/dist/dev.js +48 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +6 -6
- package/dist/otel/otlp.d.ts +103 -0
- package/dist/otel/otlp.js +385 -0
- package/package.json +7 -1
- package/dist/otel/otlp-http.d.ts +0 -116
- package/dist/otel/otlp-http.js +0 -322
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
|
|
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,
|
|
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:
|
|
662
|
-
|
|
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,
|
|
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 {
|
|
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.
|
|
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, "
|
|
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; } });
|
package/dist/otel/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { SpanExporter } from "./exporter.js";
|
|
2
|
-
export {
|
|
2
|
+
export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, toReadableSpan, type OtlpExporterOptions, type OtlpProtocol, } from "./otlp.js";
|
package/dist/otel/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "
|
|
6
|
-
Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return
|
|
7
|
-
Object.defineProperty(exports, "
|
|
8
|
-
Object.defineProperty(exports, "
|
|
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.
|
|
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
|
},
|
package/dist/otel/otlp-http.d.ts
DELETED
|
@@ -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 {};
|
package/dist/otel/otlp-http.js
DELETED
|
@@ -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
|
-
}
|