-
Notifications
You must be signed in to change notification settings - Fork 42
Feat/109 distributed tracing #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import axios from 'axios'; | ||
|
|
||
| const REGISTRY_URL = process.env.REGISTRY_URL || 'http://localhost:3000'; | ||
| const AGENT_ID = process.env.AGENT_ID || 'agent-core-01'; | ||
|
|
||
| /** | ||
| * Registers startup configuration and provisions automatic lifecycle loops | ||
| */ | ||
| export function initializeAgentHeartbeat() { | ||
| console.log(`[Lifecycle] Initializing background pulse telemetry for Agent: ${AGENT_ID}`); | ||
|
|
||
| // Establish heartbeat cycle matching the 60-second execution window specifications | ||
| const heartbeatInterval = setInterval(async () => { | ||
| try { | ||
| await axios.post(`${REGISTRY_URL}/agents/${AGENT_ID}/heartbeat`); | ||
| } catch (error: any) { | ||
| console.warn(`[Lifecycle Warning] Pulse telemetry transmission dropped: ${error.message}`); | ||
| } | ||
| }, 60000); | ||
|
|
||
| // Structural Cleanup Management: Clear event pools if process triggers termination signals | ||
| process.on('SIGTERM', () => { | ||
| clearInterval(heartbeatInterval); | ||
| }); | ||
| process.on('SIGINT', () => { | ||
| clearInterval(heartbeatInterval); | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,82 @@ | ||
| import { trace } from '@opentelemetry/api'; | ||
|
|
||
| type LogLevel = 'info' | 'warn' | 'error' | 'debug'; | ||
|
|
||
| interface TraceContext { | ||
| traceId?: string; | ||
| spanId?: string; | ||
| } | ||
|
|
||
| function getTraceContext(): TraceContext { | ||
| const span = trace.getActiveSpan(); | ||
|
|
||
| if (!span) { | ||
| return {}; | ||
| } | ||
|
|
||
| const spanContext = span.spanContext(); | ||
|
|
||
| if (!spanContext.traceId) { | ||
| return {}; | ||
| } | ||
|
|
||
| return { | ||
| traceId: spanContext.traceId, | ||
| spanId: spanContext.spanId, | ||
| }; | ||
| } | ||
|
|
||
| function log(level: LogLevel, message: string, data?: unknown) { | ||
| const ts = new Date().toISOString(); | ||
| const traceContext = getTraceContext(); | ||
|
|
||
| const prefix = `[${ts}] [${level.toUpperCase()}]`; | ||
|
|
||
| const context = | ||
| Object.keys(traceContext).length > 0 | ||
| ? traceContext | ||
| : undefined; | ||
|
|
||
| if (data !== undefined && context !== undefined) { | ||
| console.log( | ||
| `${prefix} ${message}`, | ||
| { | ||
| ...context, | ||
| data, | ||
| }, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (data !== undefined) { | ||
| console.log(`${prefix} ${message}`, data); | ||
| } else { | ||
| console.log(`${prefix} ${message}`); | ||
| console.log( | ||
| `${prefix} ${message}`, | ||
| data, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (context !== undefined) { | ||
| console.log( | ||
| `${prefix} ${message}`, | ||
| context, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`${prefix} ${message}`); | ||
| } | ||
|
|
||
| export const logger = { | ||
| info: (msg: string, data?: unknown) => log('info', msg, data), | ||
| warn: (msg: string, data?: unknown) => log('warn', msg, data), | ||
| error: (msg: string, data?: unknown) => log('error', msg, data), | ||
| debug: (msg: string, data?: unknown) => log('debug', msg, data), | ||
| }; | ||
| info: (msg: string, data?: unknown) => | ||
| log('info', msg, data), | ||
|
|
||
| warn: (msg: string, data?: unknown) => | ||
| log('warn', msg, data), | ||
|
|
||
| error: (msg: string, data?: unknown) => | ||
| log('error', msg, data), | ||
|
|
||
| debug: (msg: string, data?: unknown) => | ||
| log('debug', msg, data), | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| /** | ||
| * CleverCon OpenTelemetry semantic attribute names. | ||
| * | ||
| * Keep these attributes: | ||
| * - bounded | ||
| * - searchable | ||
| * - non-sensitive | ||
| * - low-cardinality where possible | ||
| */ | ||
|
|
||
| export const TRACE_ATTRIBUTES = { | ||
| TASK_ID: 'clevercon.task.id', | ||
| STEP_ID: 'clevercon.step.id', | ||
| AGENT_ID: 'clevercon.agent.id', | ||
| ASSET: 'clevercon.asset', | ||
| AMOUNT: 'clevercon.amount', | ||
|
|
||
| AGENT_PROTOCOL: 'clevercon.agent.protocol', | ||
|
|
||
| VAULT_NETWORK: 'clevercon.vault.network', | ||
| VAULT_OPERATION: 'clevercon.vault.operation', | ||
| } as const; | ||
|
|
||
| export type TraceAttributes = Record< | ||
| string, | ||
| string | number | boolean | undefined | ||
| >; | ||
|
|
||
| function boundedString( | ||
| value: unknown, | ||
| maxLength = 128, | ||
| ): string | undefined { | ||
| if (value === undefined || value === null) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const stringValue = String(value); | ||
|
|
||
| if (!stringValue) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return stringValue.slice(0, maxLength); | ||
| } | ||
|
|
||
| export function taskAttributes(taskId: unknown): TraceAttributes { | ||
| const value = boundedString(taskId); | ||
|
|
||
| return value | ||
| ? { | ||
| [TRACE_ATTRIBUTES.TASK_ID]: value, | ||
| } | ||
| : {}; | ||
| } | ||
|
|
||
| export function stepAttributes( | ||
| taskId: unknown, | ||
| stepId: unknown, | ||
| ): TraceAttributes { | ||
| return { | ||
| ...taskAttributes(taskId), | ||
| ...(boundedString(stepId) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.STEP_ID]: boundedString(stepId), | ||
| } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
| export function agentAttributes( | ||
| taskId: unknown, | ||
| stepId: unknown, | ||
| agentId: unknown, | ||
| protocol?: unknown, | ||
| ): TraceAttributes { | ||
| return { | ||
| ...stepAttributes(taskId, stepId), | ||
|
|
||
| ...(boundedString(agentId) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.AGENT_ID]: boundedString(agentId), | ||
| } | ||
| : {}), | ||
|
|
||
| ...(boundedString(protocol) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.AGENT_PROTOCOL]: boundedString(protocol, 32), | ||
| } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
| export function vaultAttributes(options: { | ||
| taskId?: unknown; | ||
| stepId?: unknown; | ||
| agentId?: unknown; | ||
| asset?: unknown; | ||
| amount?: unknown; | ||
| network?: unknown; | ||
| operation?: unknown; | ||
| }): TraceAttributes { | ||
| return { | ||
| ...stepAttributes(options.taskId, options.stepId), | ||
|
|
||
| ...(boundedString(options.agentId) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.AGENT_ID]: boundedString(options.agentId), | ||
| } | ||
| : {}), | ||
|
|
||
| ...(boundedString(options.asset, 32) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.ASSET]: boundedString(options.asset, 32), | ||
| } | ||
| : {}), | ||
|
|
||
| ...(options.amount !== undefined && options.amount !== null | ||
| ? { | ||
| [TRACE_ATTRIBUTES.AMOUNT]: boundedString(options.amount, 64), | ||
| } | ||
| : {}), | ||
|
|
||
| ...(boundedString(options.network, 32) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.VAULT_NETWORK]: boundedString( | ||
| options.network, | ||
| 32, | ||
| ), | ||
| } | ||
| : {}), | ||
|
|
||
| ...(boundedString(options.operation, 64) | ||
| ? { | ||
| [TRACE_ATTRIBUTES.VAULT_OPERATION]: boundedString( | ||
| options.operation, | ||
| 64, | ||
| ), | ||
| } | ||
| : {}), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /** | ||
| * OpenTelemetry configuration. | ||
| * | ||
| * Tracing is intentionally disabled by default. | ||
| * | ||
| * Environment variables: | ||
| * | ||
| * OTEL_TRACING_ENABLED=true | ||
| * OTEL_SERVICE_NAME=clevercon-orchestrator | ||
| * OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces | ||
| * OTEL_TRACING_EXPORTER=console|otlp | ||
| */ | ||
|
|
||
| export type TracingExporter = 'console' | 'otlp'; | ||
|
|
||
| export interface TracingConfig { | ||
| enabled: boolean; | ||
| serviceName: string; | ||
| exporter: TracingExporter; | ||
| otlpEndpoint?: string; | ||
| } | ||
|
|
||
| function parseBoolean(value: string | undefined): boolean { | ||
| if (!value) { | ||
| return false; | ||
| } | ||
|
|
||
| return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); | ||
| } | ||
|
|
||
| function getExporter(): TracingExporter { | ||
| const configured = process.env.OTEL_TRACING_EXPORTER?.toLowerCase(); | ||
|
|
||
| if (configured === 'otlp') { | ||
| return 'otlp'; | ||
| } | ||
|
|
||
| if (configured === 'console') { | ||
| return 'console'; | ||
| } | ||
|
|
||
| return process.env.OTEL_EXPORTER_OTLP_ENDPOINT ? 'otlp' : 'console'; | ||
| } | ||
|
|
||
| export function getTracingConfig(): TracingConfig { | ||
| return { | ||
| enabled: parseBoolean(process.env.OTEL_TRACING_ENABLED), | ||
| serviceName: | ||
| process.env.OTEL_SERVICE_NAME?.trim() || 'clevercon-service', | ||
| exporter: getExporter(), | ||
| otlpEndpoint: | ||
| process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || undefined, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { | ||
| context, | ||
| propagation, | ||
| trace, | ||
| Span, | ||
| SpanStatusCode, | ||
| type Context, | ||
| } from '@opentelemetry/api'; | ||
|
|
||
| export const TRACER_NAME = 'clevercon'; | ||
|
|
||
| export const tracer = trace.getTracer(TRACER_NAME); | ||
|
|
||
| export function startSpan<T>( | ||
| name: string, | ||
| attributes: Record<string, string | number | boolean>, | ||
| fn: (span: Span) => Promise<T> | T, | ||
| ): Promise<T> | T { | ||
| return tracer.startActiveSpan(name, { attributes }, async (span) => { | ||
| try { | ||
| return await fn(span); | ||
| } catch (error) { | ||
| span.recordException(error as Error); | ||
| span.setStatus({ | ||
| code: SpanStatusCode.ERROR, | ||
| message: error instanceof Error ? error.message : String(error), | ||
| }); | ||
|
|
||
| throw error; | ||
| } finally { | ||
| span.end(); | ||
| } | ||
| }); | ||
|
Comment on lines
+14
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline packages/common/src/tracing --items all --type function
rg -n -P --glob '*.ts' -C 3 \
"import\s*\{[^}]*\b(startSpan|runInSpan|extractTraceContext)\b|\\b(startSpan|runInSpan|extractTraceContext)\s*\(" \
packagesRepository: clevercon-protocol/clevercon Length of output: 1207 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- tracing files ---'
git ls-files packages/common/src/tracing packages/common/src | sed -n '1,160p'
printf '%s\n' '--- tracing source exports ---'
rg -n -C 3 '^(export|export \{)|from .*tracing|tracing' packages/common/src packages/common/package.json package.json 2>/dev/null | sed -n '1,240p'
printf '%s\n' '--- candidate package entry points ---'
fd -t f -i 'index.ts|package.json|tsconfig*.json|exports*' packages/common | sort | sed -n '1,160p'Repository: clevercon-protocol/clevercon Length of output: 14478 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- packages/common/package.json ---'
cat -n packages/common/package.json
printf '%s\n' '--- packages/common/src/index.ts ---'
cat -n packages/common/src/index.ts
printf '%s\n' '--- tracing module exports ---'
for file in packages/common/src/tracing/index.ts packages/common/src/tracing/propagation.ts packages/common/src/tracing/tracer.ts; do
printf '\n--- %s ---\n' "$file"
cat -n "$file" | sed -n '1,150p'
done
printf '%s\n' '--- imports from the common package or tracing subpath ---'
rg -n -P --glob '*.ts' --glob '*.tsx' \
"from ['\"][^'\"]*(common|tracing)(/[^'\"]*)?['\"]|require\(['\"][^'\"]*(common|tracing)(/[^'\"]*)?['\"]\)" \
packages | sed -n '1,240p'Repository: clevercon-protocol/clevercon Length of output: 9006 Expose context-aware tracing through The package exports only 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align the heartbeat transport contract. The default configuration targets port 3000, while the registry defaults to port 4000, and the configured heartbeat route has no registry handler.
.env.example#L152-L152: defineREGISTRY_URLonce and point it to the registry service.packages/agent_shared_directory/src/server.ts#L15-L15: call a registry route that exists, or addPOST /agents/:agentId/heartbeatto update agent freshness.🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 152-152: [DuplicatedKey] The REGISTRY_URL key is duplicated
(DuplicatedKey)
📍 Affects 2 files
.env.example#L152-L152(this comment)packages/agent_shared_directory/src/server.ts#L15-L15🤖 Prompt for AI Agents
Source: Linters/SAST tools