Skip to content

Commit 5fb0159

Browse files
kirang89claude
andcommitted
Add OTLP tracing endpoint flag and config key
New --tracing-endpoint flag (and tracingEndpoint config key) wires an OTLP/HTTP traces exporter into a NodeTracerProvider so spans emitted by @nilenso/megasthenes are shipped to Arize Phoenix (or any OTLP-compatible collector). The URL is normalised to append /v1/traces when a bare base URL is given. Spans are flushed via shutdownTracing() in the ask command's finally block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c709987 commit 5fb0159

5 files changed

Lines changed: 82 additions & 1 deletion

File tree

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252
},
5353
"dependencies": {
5454
"@nilenso/megasthenes": "file:../ask-forge",
55+
"@opentelemetry/api": "^1.9.1",
56+
"@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
57+
"@opentelemetry/resources": "^2.7.0",
58+
"@opentelemetry/sdk-trace-base": "^2.7.0",
59+
"@opentelemetry/sdk-trace-node": "^2.7.0",
60+
"@opentelemetry/semantic-conventions": "^1.40.0",
5561
"marked": "^15.0.0",
5662
"marked-terminal": "^7.2.1"
5763
},

src/cli/ask-args.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export interface ParsedArgs {
3030
sandboxTimeoutMs?: number;
3131
sandboxSecret?: string;
3232

33+
// Tracing
34+
tracingEndpoint?: string;
35+
3336
// Output
3437
verbose: boolean;
3538
json: boolean;
@@ -120,6 +123,10 @@ export function parseAskArgs(argv: readonly string[]): ParsedArgs {
120123
out.sandboxSecret = consume();
121124
break;
122125

126+
case "--tracing-endpoint":
127+
out.tracingEndpoint = consume();
128+
break;
129+
123130
case "--verbose":
124131
out.verbose = true;
125132
break;
@@ -171,6 +178,11 @@ Sandbox options:
171178
--sandbox-timeout-ms <ms> Per-request timeout for sandbox calls.
172179
--sandbox-secret <s> Shared secret for sandbox auth.
173180
181+
Tracing options:
182+
--tracing-endpoint <url> OTLP/HTTP traces endpoint (e.g. Arize Phoenix at
183+
http://localhost:6006). The path "/v1/traces" is
184+
appended automatically if omitted.
185+
174186
Output options:
175187
--verbose Stream tool calls, iteration starts, and errors to stderr.
176188
--json Emit the full TurnResult as JSON to stdout instead of markdown.

src/cli/ask-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface ResolvedConfig {
2929
question: string;
3030
verbose: boolean;
3131
json: boolean;
32+
tracingEndpoint?: string;
3233
}
3334

3435
export type FileConfig = Partial<
@@ -48,6 +49,7 @@ export type FileConfig = Partial<
4849
| "sandboxBaseUrl"
4950
| "sandboxTimeoutMs"
5051
| "sandboxSecret"
52+
| "tracingEndpoint"
5153
>
5254
>;
5355

@@ -71,6 +73,7 @@ const ALLOWED_FILE_KEYS = [
7173
"sandboxBaseUrl",
7274
"sandboxTimeoutMs",
7375
"sandboxSecret",
76+
"tracingEndpoint",
7477
] as const;
7578

7679
export function defaultConfigPath(env: NodeJS.ProcessEnv): string | undefined {
@@ -148,6 +151,7 @@ export function parseFileConfig(raw: string, path: string): FileConfig {
148151
setString("systemPromptFile");
149152
setString("sandboxBaseUrl");
150153
setString("sandboxSecret");
154+
setString("tracingEndpoint");
151155
setPosInt("maxIterations");
152156
setPosInt("sandboxTimeoutMs");
153157
setBool("verbose");
@@ -211,13 +215,16 @@ export function resolveConfig(
211215
logger: nullLogger,
212216
};
213217

218+
const tracingEndpoint = args.tracingEndpoint ?? file.tracingEndpoint;
219+
214220
return {
215221
clientConfig,
216222
sessionConfig,
217223
askOptions: {},
218224
question: args.question,
219225
verbose: args.verbose || (file.verbose ?? false),
220226
json: args.json || (file.json ?? false),
227+
...(tracingEndpoint !== undefined ? { tracingEndpoint } : {}),
221228
};
222229
}
223230

src/cli/ask.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ASK_HELP, ArgParseError, parseAskArgs } from "./ask-args.ts";
1111
import { resolveConfig } from "./ask-config.ts";
1212
import { extractFinalAnswer, formatSummary, renderMarkdown } from "./ask-render.ts";
1313
import { findMissingTools, formatMissingToolsError } from "./check-tools.ts";
14+
import { setupTracing, shutdownTracing } from "./tracing.ts";
1415

1516
const ERROR_EXIT_CODES: Record<ErrorType, number> = {
1617
internal_error: 1,
@@ -50,7 +51,13 @@ export async function runAsk(argv: readonly string[]): Promise<number> {
5051
throw e;
5152
}
5253

53-
const { clientConfig, sessionConfig, askOptions, question, verbose, json } = resolved;
54+
const { clientConfig, sessionConfig, askOptions, question, verbose, json, tracingEndpoint } =
55+
resolved;
56+
57+
if (tracingEndpoint !== undefined) {
58+
setupTracing(tracingEndpoint);
59+
if (verbose) process.stderr.write(`[tracing] OTLP endpoint: ${tracingEndpoint}\n`);
60+
}
5461

5562
// Preflight: in local mode the library shells out to git/rg/fd. Skip when
5663
// running against a sandbox worker (those tools live in the worker, not here).
@@ -111,6 +118,8 @@ export async function runAsk(argv: readonly string[]): Promise<number> {
111118
} catch {
112119
// ignore close errors
113120
}
121+
// Flush pending spans before the process exits.
122+
await shutdownTracing();
114123
}
115124
}
116125

src/cli/tracing.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* OTLP/HTTP tracing setup. When an endpoint is configured, register a global
3+
* tracer provider so spans emitted by @nilenso/megasthenes (via
4+
* @opentelemetry/api) are exported to a collector such as Arize Phoenix,
5+
* Langfuse, Jaeger, or any OTLP-compatible receiver.
6+
*
7+
* The library uses trace.getTracer() lazily on each span creation, so
8+
* registering the provider before client.connect() is sufficient.
9+
*/
10+
11+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
12+
import { resourceFromAttributes } from "@opentelemetry/resources";
13+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
14+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
15+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
16+
17+
let provider: NodeTracerProvider | undefined;
18+
19+
/**
20+
* Resolve a user-provided endpoint to the full OTLP/HTTP traces URL. Accepts
21+
* either a base URL (`http://localhost:6006`) — in which case `/v1/traces` is
22+
* appended — or a fully-qualified traces URL.
23+
*/
24+
export function resolveTracesUrl(endpoint: string): string {
25+
const trimmed = endpoint.replace(/\/$/, "");
26+
return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
27+
}
28+
29+
export function setupTracing(endpoint: string): void {
30+
if (provider !== undefined) return;
31+
const url = resolveTracesUrl(endpoint);
32+
provider = new NodeTracerProvider({
33+
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "megasthenes-cli" }),
34+
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url }))],
35+
});
36+
provider.register();
37+
}
38+
39+
export async function shutdownTracing(): Promise<void> {
40+
if (provider === undefined) return;
41+
try {
42+
await provider.shutdown();
43+
} catch {
44+
// ignore shutdown errors — best-effort flush
45+
}
46+
provider = undefined;
47+
}

0 commit comments

Comments
 (0)