Skip to content

feat(intelligence): OpenTelemetry token usage collection pipeline - #9862

Open
rwhite27 wants to merge 17 commits into
developmentfrom
feat/opentelemetry-token-usage
Open

rwhite27 wants to merge 17 commits into
developmentfrom
feat/opentelemetry-token-usage

Conversation

@rwhite27

@rwhite27 rwhite27 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replaces SSH polling for LLM token counts with a push-based OTel pipeline — no more CollectMachineTelemetryJob SSH usage calls when enabled
  • Agent containers auto-instrument Anthropic/OpenAI SDK calls via @traceloop/instrumentation-anthropic; spans flow to an OTel Collector that aggregates gen_ai.usage.* attributes and forwards metrics to a new Laravel OTLP receiver
  • Data lands in the existing agent_usage_snapshots table with source = '<provider>_otel', fully backwards-compatible with SSH-polled rows

Key components

File Role
docker/otel/collector.yaml OTLP gRPC receiver → spanmetrics connector → OTLP/HTTP exporter to Laravel
docker/otel/otel-init.js Node.js OTel SDK bootstrap injected via NODE_OPTIONS=--require in container Dockerfiles
docker-compose.yml New otel-collector service (ports 4317/4318)
config/otel.php enabled, collector_endpoint, internal_token feature flags
BaseDockerComposeBuilderService Injects OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_SERVICE_NAME into agent containers when enabled
OtlpAdapterController Thin REST bridge (validates X-Internal-Token) forwarding OTLP JSON to GraphQL
ingestAgentTelemetry mutation Lighthouse mutation resolver → OtlpUsageIngestionService
OtlpUsageIngestionService Parses OTLP metrics JSON, resolves deployment by ID or container name, upserts AgentUsageSnapshot
CollectDeploymentUsageCommand Skip SSH poll when otel.enabled = true
Kernel.php Enable CollectAgentTelemetryCommand cron (was commented out)

Enabling

Add to .env:

OTEL_ENABLED=true
OTEL_INTERNAL_TOKEN=<random-secret>   # same value in both .env and docker-compose.yml

Restart the stack — otel-collector will start alongside the existing services.

Containers (Dockerfile change needed)

Add this layer to each agent container Dockerfile (openclaw, hermes):

RUN npm install --save \
    @opentelemetry/sdk-node \
    @opentelemetry/exporter-trace-otlp-grpc \
    @opentelemetry/resources \
    @traceloop/instrumentation-anthropic

COPY docker/otel/otel-init.js /app/otel-init.js
ENV NODE_OPTIONS="--require /app/otel-init.js"

Test plan

  • Start stack with OTEL_ENABLED=true — otel-collector container is healthy
  • Make an LLM call through a running agent — row appears in agent_usage_snapshots with source = 'openclaw_otel' within 30s
  • Set OTEL_ENABLED=false — SSH polling still creates rows with source = 'openclaw_docker'
  • Confirm no double-counting: both sources active → two separate rows per deployment/date
  • Debug mode: uncomment debug exporter in collector.yaml, check docker logs otel-collector shows received spans

🤖 Generated with Claude Code

rwhite27 and others added 14 commits May 21, 2026 10:03
…ield

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n, and relax deployment validation

- Fix CreateAgentTypeAction updateOrCreate to use apps_id+name as the match key
- Fix AgentType model fillable: app_id → apps_id
- Add latestDeployment relation on Agent model and expose it in GraphQL schema
- Relax multi_agent_list DTO type from ?string to mixed
- Remove blocking validation in AgentChannelIntegrationReadinessService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tegrationReadinessService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces SSH polling for LLM token counts with a push-based OTel pipeline.
Agent containers auto-instrument Anthropic/OpenAI calls via the OTel SDK;
spans flow to an OTel Collector which aggregates gen_ai.usage.* attributes
into metrics and forwards them to a new Laravel OTLP receiver stored in
agent_usage_snapshots (source = '<provider>_otel').

- docker/otel/collector.yaml: OTLP gRPC receiver → spanmetrics connector → OTLP/HTTP exporter
- docker/otel/otel-init.js: Node.js OTel SDK bootstrap (AnthropicInstrumentation auto-instruments LLM calls, uses KANVAS_DEPLOYMENT_ID resource attr)
- docker-compose.yml: add otel-collector service (ports 4317/4318)
- config/otel.php: enabled flag, collector_endpoint, internal_token
- BaseDockerComposeBuilderService: inject OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_SERVICE_NAME when otel.enabled=true
- OtlpAdapterController: thin REST bridge (X-Internal-Token auth) that forwards OTLP JSON to GraphQL mutation
- ingestAgentTelemetry mutation + IngestOtlpMetricsMutation resolver
- OtlpUsageIngestionService: parses OTLP metrics JSON, resolves deployment by ID or container_name, upserts AgentUsageSnapshot
- CollectDeploymentUsageCommand: skip SSH poll when otel.enabled=true
- Kernel.php: enable CollectAgentTelemetryCommand cron + add hourly usage snapshot schedule

Enable via: OTEL_ENABLED=true, OTEL_INTERNAL_TOKEN=<secret>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…make inferLlmProvider public

- CollectAllDeploymentsUsageCommand was referenced in Kernel.php but not committed
- inferLlmProvider() changed from protected to public static so OtlpUsageIngestionService can call it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… into feat/opentelemetry-token-usage

# Conflicts:
#	app/Console/Kernel.php
…token usage ingestion

- Dockerfile templates: add {{OTEL_LAYER}} placeholder; buildDockerfile() expands it
  to a RUN layer that installs OTel packages at /opt/otel when otel.enabled=true
- BaseLaunchAgentOnMachineAction: write otel-init.js to Docker build context when enabled
- BaseDockerComposeBuilderService: inject NODE_OPTIONS=--require /opt/otel/init.js
  alongside OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_SERVICE_NAME when enabled
- otel-init.js: absolute requires from /opt/otel/node_modules, bail-out guard on
  OTEL_EXPORTER_OTLP_ENDPOINT, use KANVAS_DEPLOYMENT_ID resource attribute
- collector.yaml: drop spanmetrics (only produces calls/duration, not token counts);
  forward raw gen_ai.* spans directly to Laravel via traces_endpoint; batch timeout 10s
- OtlpUsageIngestionService: primary path parses OTLP traces (resourceSpans) extracting
  gen_ai.usage.* attributes per span, aggregated per deployment; legacy metrics path kept
  for backward compat; upsertUsageSnapshot() shared by both paths; raw_output='' fix
- OtlpAdapterController: call OtlpUsageIngestionService directly (no GraphQL tunnel);
  GraphQL @guard blocked system calls without auth context
- docker-compose.yml: otel-collector depends_on php so first export doesn't hit cold target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwhite27
rwhite27 changed the base branch from hermes-agents-feature to development June 11, 2026 12:07
rwhite27 and others added 3 commits June 11, 2026 08:27
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- seed_default_tool_categories: add uuid generation to inserts
- swap_tool_category_enum_for_fk: guard ADD COLUMN and DROP COLUMN with hasColumn checks
- add_is_sub_agent: guard both column additions with hasColumn checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant