Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ts/config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -400,14 +400,18 @@ typeagent:
# - otlpEndpoint fallback endpoint for traces + metrics + logs
# - logFile local JSONL log file; enables the logs signal
# independently of any OTLP endpoint. Leading
# `~`/`~/`/`~\` expands to your home dir.
# `~`/`~/`/`~\` expands to your home dir. Supports
# `{service}`, `{process}`, and `{pid}` placeholders.
# - debugBridge copy enabled typeagent:* debug output to OTel logs
# - tracesSampler one of always_on, always_off, traceidratio,
# parentbased_always_on, parentbased_always_off,
# parentbased_traceidratio
# - tracesSamplerArg required with the ratio samplers (0.0 - 1.0)

# telemetry:
# otlpEndpoint: http://localhost:4318
# logFile: ~/.typeagent/logs/typeagent-{service}-{pid}.jsonl
# logFile: ~/.typeagent/logs/typeagent-{service}-{process}-{pid}.jsonl
# debugBridge: true
# structuredLogs: true
# tracesSampler: parentbased_traceidratio
# tracesSamplerArg: 0.1
303 changes: 293 additions & 10 deletions ts/docs/architecture/telemetry/opentelemetry.md

Large diffs are not rendered by default.

19 changes: 9 additions & 10 deletions ts/examples/workflow/lsp/test/serverIntegration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
DefinitionRequest,
RenameRequest,
DocumentRangeFormattingRequest,
ShutdownRequest,
ExitNotification,
StreamMessageReader,
StreamMessageWriter,
} from "vscode-languageserver-protocol/node.js";
Expand Down Expand Up @@ -67,16 +69,13 @@ async function startSession(debounceMs = 5) {
server,
publishes,
cleanup: async () => {
// Add a small delay to let pending operations and error handlers complete
// before disposing connections. This prevents "Connection is disposed" errors
// when error handlers try to send notifications after test completion.
await new Promise((resolve) => setTimeout(resolve, 50));
client.dispose();
server.dispose();
pipes.serverTransport.input.destroy();
pipes.serverTransport.output.destroy();
pipes.clientReader.dispose();
pipes.clientWriter.dispose();
try {
await client.sendRequest(ShutdownRequest.type);
await client.sendNotification(ExitNotification.type);
} finally {
server.dispose();
client.dispose();
}
},
};
}
Expand Down
1 change: 1 addition & 0 deletions ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"start:agent-server:tunnel": "pnpm -C packages/agentServer/server run start:tunnel",
"start:mcp": "pnpm -C packages/commandExecutor run start",
"stop:agent-server": "pnpm -C packages/agentServer/server run stop",
"telemetry:grafana": "node tools/scripts/startLocalTelemetry.mjs",
"test": "pnpm run test:local && pnpm run test:live && pnpm run shell:test",
"test:keys": "npx tsx tools/scripts/testServiceKeys.ts",
"test:live": "pnpm -r ---no-bail -no-sort --stream --workspace-concurrency=1 run test:live",
Expand Down
13 changes: 12 additions & 1 deletion ts/packages/agentServer/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,15 @@ process.once("message", (message) => {
// Load config from YAML layers + Key Vault (replacing legacy dotenv).
// vault.shared is auto-discovered from config.local.yaml / config.defaults.yaml.
await loadConfig({ keyVault: {}, strict: false });
const telemetryInit = otel.initTelemetry();
const telemetryConfig = otel.resolveTelemetryConfig();
const telemetryInit = otel.initTelemetry({
config: telemetryConfig,
processName: "agent-server",
debugModules: [registerDebug],
debugBridge: {
includedNamespacePrefixes: ["typeagent:", "agent-server:"],
},
});

// Snapshot whether this server's local config differs from the shared Key
// Vault, so clients can be warned on connect (same delivery path as the
Expand Down Expand Up @@ -342,6 +350,9 @@ async function main() {
dblogging: true,
developerMode,
traceId,
telemetry: {
structuredLogs: telemetryConfig.structuredLogs === true,
},
indexingServiceRegistry: await getIndexingServiceRegistry(
instanceDir,
configName,
Expand Down
13 changes: 11 additions & 2 deletions ts/packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { TypeAgentServer } from "./typeAgentServer.js";
import { loadConfig } from "@typeagent/config";
import { otel } from "@typeagent/telemetry";
import registerDebug from "debug";

let typeAgentServer: TypeAgentServer | undefined;
let shutdownPromise: Promise<void> | undefined;
Expand Down Expand Up @@ -40,14 +41,22 @@ process.once("SIGTERM", () => {
async function main(): Promise<void> {
// Load config from YAML layers + Key Vault (replacing legacy dotenv).
await loadConfig({ keyVault: {}, strict: false });
await otel.initTelemetry();
const telemetryConfig = otel.resolveTelemetryConfig();
await otel.initTelemetry({
config: telemetryConfig,
processName: "api-server",
debugModules: [registerDebug],
debugBridge: {
includedNamespacePrefixes: ["typeagent:", "agent-server:"],
},
});
if (shutdownRequested) {
return;
}

typeAgentServer = new TypeAgentServer((exitCode) => {
void shutdownHost(exitCode);
});
}, telemetryConfig.structuredLogs === true);

await typeAgentServer.start();
}
Expand Down
7 changes: 5 additions & 2 deletions ts/packages/api/src/typeAgentServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export class TypeAgentServer {
private storageProvider: TypeAgentStorageProvider | undefined;
private config: TypeAgentAPIServerConfig;

constructor(private readonly requestExit: (exitCode: number) => void) {
constructor(
private readonly requestExit: (exitCode: number) => void,
private readonly structuredLogs: boolean,
) {
// Build typed runtime Config from process.env (already populated
// by loadConfig in the entry point) so aiclient consumers can
// use the typed accessor; legacy callers still see the same
Expand Down Expand Up @@ -83,7 +86,7 @@ export class TypeAgentServer {
sw.stop("Downloaded Session Backup");
}

this.webDispatcher = await createWebDispatcher();
this.webDispatcher = await createWebDispatcher(this.structuredLogs);
debug("Web Dispatcher created.");

// web server
Expand Down
7 changes: 6 additions & 1 deletion ts/packages/api/src/webDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export interface WebDispatcher {
handleAction(action: FullAction): Promise<CommandResult>;
}

export async function createWebDispatcher(): Promise<WebDispatcher> {
export async function createWebDispatcher(
structuredLogs: boolean,
): Promise<WebDispatcher> {
let ws: WebSocket | null = null;
const clientIOChannel = createChannelAdapter((message: any) =>
ws?.send(
Expand Down Expand Up @@ -64,6 +66,9 @@ export async function createWebDispatcher(): Promise<WebDispatcher> {
metrics: true,
dblogging: true,
traceId: getTraceId(),
telemetry: {
structuredLogs,
},
clientIO: clientIO,
constructionProvider: getDefaultConstructionProvider(),
indexingServiceRegistry: await getIndexingServiceRegistry(instanceDir),
Expand Down
36 changes: 22 additions & 14 deletions ts/packages/cache/src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,15 @@ export class AgentCache {
message: `Grammar generation error: ${error.message}`,
};

this.logger?.logEvent("grammarGeneration", {
request: requestAction.request,
success: false,
error: error.message,
});
this.logger?.logEvent(
"grammarGeneration",
{
request: requestAction.request,
success: false,
error: error.message,
},
"error",
);
}
}

Expand All @@ -518,15 +522,19 @@ export class AgentCache {
...(grammarResult !== undefined && { grammarResult }),
};
} catch (e: any) {
this.logger?.logEvent("error", {
request: requestAction.request,
actions: requestAction.actions,
history: requestAction.history,
cache,
options,
message: e.message,
stack: e.stack,
});
this.logger?.logEvent(
"error",
{
request: requestAction.request,
actions: requestAction.actions,
history: requestAction.history,
cache,
options,
message: e.message,
stack: e.stack,
},
"error",
);
throw e;
}
}
Expand Down
6 changes: 5 additions & 1 deletion ts/packages/cli/bin/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { loadConfigSync } from "@typeagent/config";
import { otel } from "@typeagent/telemetry";
import registerDebug from "debug";
import { registerEarlyTelemetrySignalHandlers } from "../src/telemetry.ts";
loadConfigSync();

Expand All @@ -15,7 +16,10 @@ async function main() {
process.env.NODE_ENV = "development";
settings.debug = true;
try {
await otel.initTelemetry();
await otel.initTelemetry({
processName: "cli",
debugModules: [registerDebug],
});
await run(process.argv.slice(2), import.meta.url);
await flush();
await otel.shutdownTelemetry();
Expand Down
6 changes: 5 additions & 1 deletion ts/packages/cli/bin/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { loadConfigSync } from "@typeagent/config";
import { otel } from "@typeagent/telemetry";
import registerDebug from "debug";
import { registerEarlyTelemetrySignalHandlers } from "../dist/telemetry.js";
loadConfigSync();

Expand All @@ -12,7 +13,10 @@ registerEarlyTelemetrySignalHandlers();
async function main() {
const { flush, handle, run } = await import("@oclif/core");
try {
await otel.initTelemetry();
await otel.initTelemetry({
processName: "cli",
debugModules: [registerDebug],
});
await run(process.argv.slice(2), import.meta.url);
await flush();
await otel.shutdownTelemetry();
Expand Down
14 changes: 9 additions & 5 deletions ts/packages/dispatcher/dispatcher/src/command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,15 @@ export async function processCommandNoLock(
);
debugCommandError(e.stack);

context?.logger?.logEvent("command:exception", {
request: originalInput,
message: e.message,
stack: e.stack,
});
context?.logger?.logEvent(
"command:exception",
{
request: originalInput,
message: e.message,
stack: e.stack,
},
"error",
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
ensureDirectory,
lockInstanceDir,
} from "../utils/fsUtils.js";
import { createDispatcherOtelLoggerSink } from "../otel/structuredLogSink.js";
import {
ActionContext,
AppAgentEvent,
Expand Down Expand Up @@ -599,6 +600,11 @@ export type DispatcherOptions = DeepPartialUndefined<DispatcherConfig> & {
* Default false: each request starts a new trace.
*/
joinActiveTrace?: boolean;
/**
* Export structured dispatcher events through the global OTel logs
* provider. Default false because event payloads may contain user data.
*/
structuredLogs?: boolean;
};

// Additional integration options
Expand Down Expand Up @@ -727,7 +733,11 @@ function getCosmosFactories(): PromptLoggerOptions {
return result;
}

function getLoggerSink(isDbEnabled: () => boolean, clientIO: ClientIO) {
function getLoggerSink(
isDbEnabled: () => boolean,
clientIO: ClientIO,
structuredLogs: boolean,
) {
const debugLoggerSink = createDebugLoggerSink();
let dbLoggerSink: LoggerSink | undefined;

Expand Down Expand Up @@ -759,11 +769,14 @@ function getLoggerSink(isDbEnabled: () => boolean, clientIO: ClientIO) {
);
}

return new MultiSinkLogger(
const sinks =
dbLoggerSink === undefined
? [debugLoggerSink]
: [debugLoggerSink, dbLoggerSink],
);
: [debugLoggerSink, dbLoggerSink];
if (structuredLogs) {
sinks.push(createDispatcherOtelLoggerSink());
}
return new MultiSinkLogger(sinks);
}

async function lockEmbeddingCacheDir(context: CommandHandlerContext) {
Expand Down Expand Up @@ -1206,7 +1219,11 @@ export async function initializeCommandHandlerContext(
const sessionDirPath = session.getSessionDirPath();
debug(`Session directory: ${sessionDirPath}`);
const clientIO = options?.clientIO ?? nullClientIO;
const loggerSink = getLoggerSink(() => context.dblogging, clientIO);
const loggerSink = getLoggerSink(
() => context.dblogging,
clientIO,
options?.telemetry?.structuredLogs === true,
);
const activationId = randomUUID();
const traceId = options?.traceId;
const logger = new ChildLogger(loggerSink, DispatcherName, {
Expand Down Expand Up @@ -1415,8 +1432,8 @@ export async function initializeCommandHandlerContext(
},
context.logger
? {
logEvent: (name, data) =>
context.logger?.logEvent(name, data as any),
logEvent: (name, data, severity) =>
context.logger?.logEvent(name, data as any, severity),
}
: undefined,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,17 @@ async function canTranslateWithoutContext(
newActions,
});
} catch (e: any) {
logger?.logEvent("contextlessTranslation", {
requestAction,
actions: oldActions,
history: requestAction.history,
newActions,
error: e.message,
});
logger?.logEvent(
"contextlessTranslation",
{
requestAction,
actions: oldActions,
history: requestAction.history,
newActions,
error: e.message,
},
"error",
);
throw e;
}
}
Expand Down Expand Up @@ -824,11 +828,15 @@ export class RequestCommandHandler implements CommandHandler {
DispatcherName,
);
}
systemContext?.logger?.logEvent("request:exception", {
request,
message: e.message,
stack: e.stack,
});
systemContext?.logger?.logEvent(
"request:exception",
{
request,
message: e.message,
stack: e.stack,
},
"error",
);
throw e;
}

Expand Down
Loading
Loading