Skip to content

feat(daemon): add Claude Code OTEL v2 passthrough collector#140

Merged
AnnatarHe merged 4 commits intomainfrom
feat/ccotel-otel-v2-passthrough
Dec 14, 2025
Merged

feat(daemon): add Claude Code OTEL v2 passthrough collector#140
AnnatarHe merged 4 commits intomainfrom
feat/ccotel-otel-v2-passthrough

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

Summary

  • Implement gRPC OTEL collector in daemon for Claude Code telemetry
  • Add passthrough architecture (no local buffering - immediate forwarding)
  • Parse Claude Code metrics (claude_code.token.usage, claude_code.cost.usage, etc.)
  • Parse Claude Code events (api_request, tool_result, user_prompt, etc.)
  • Forward data to backend via POST /api/v1/cc/otel

New Files

File Purpose
daemon/ccotel_server.go gRPC server implementing OTEL services
daemon/ccotel_processor.go OTEL data parsing and passthrough
model/ccotel_types.go Request/response types for backend API
model/api_ccotel.go Backend API client

Configuration

[ccotel]
enabled = true
grpcPort = 4317  # default OTEL gRPC port

User Setup

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Test plan

  • Verify daemon builds successfully
  • Verify daemon starts with [ccotel] config enabled
  • Verify gRPC server accepts OTEL data on port 4317
  • Verify data is forwarded to backend /api/v1/cc/otel

🤖 Generated with Claude Code

Implement a gRPC OTEL collector in the daemon that receives telemetry
data from Claude Code and forwards it to the ShellTime backend in
real-time (no local buffering).

- Add gRPC server implementing OTEL MetricsService and LogsService
- Parse Claude Code metrics (token usage, cost, LOC, commits, PRs)
- Parse Claude Code events (api_request, tool_result, user_prompt)
- Forward data immediately to POST /api/v1/cc/otel endpoint
- Add [ccotel] config section with enabled and grpcPort options

This is v2 of CC tracking, complementing the existing v1 ccusage
CLI-based approach which uses daily aggregates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @AnnatarHe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the daemon's telemetry capabilities by integrating a new OpenTelemetry (OTEL) gRPC collector. This system provides a standardized and efficient mechanism for collecting and immediately forwarding Claude Code usage metrics and events to a backend API. The focus is on a passthrough model, ensuring that telemetry data is transmitted in real-time for prompt analysis and monitoring.

Highlights

  • New OTEL gRPC Collector: Implemented a gRPC server within the daemon to receive OpenTelemetry (OTEL) metrics and logs specifically for Claude Code telemetry.
  • Passthrough Architecture: The system is designed for immediate forwarding of received OTEL data to a backend API (/api/v1/cc/otel) without any local buffering, ensuring real-time telemetry.
  • Claude Code Telemetry Parsing: The collector includes logic to parse and map various Claude Code-related metrics (e.g., claude_code.token.usage, claude_code.cost.usage) and events (e.g., api_request, tool_result, user_prompt) from the generic OTEL format into internal data structures.
  • Configuration and User Setup: Introduced new configuration options for [ccotel] including enabled and grpcPort (defaulting to 4317), along with clear instructions for users to set environment variables to enable and direct their OTEL exporters to the daemon.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 13, 2025

Codecov Report

❌ Patch coverage is 0.58140% with 513 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
daemon/ccotel_processor.go 0.00% 242 Missing ⚠️
model/ccotel_env.go 0.00% 163 Missing ⚠️
model/api_ccotel.go 0.00% 30 Missing ⚠️
commands/cc.go 0.00% 27 Missing ⚠️
daemon/ccotel_server.go 0.00% 26 Missing ⚠️
cmd/daemon/main.go 0.00% 15 Missing ⚠️
model/config.go 20.00% 4 Missing and 4 partials ⚠️
cmd/cli/main.go 0.00% 1 Missing ⚠️
daemon/socket.go 0.00% 1 Missing ⚠️
Flag Coverage Δ
unittests 20.64% <0.58%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
commands/track.go 61.18% <100.00%> (-3.29%) ⬇️
cmd/cli/main.go 0.00% <0.00%> (ø)
daemon/socket.go 0.00% <0.00%> (ø)
model/config.go 54.92% <20.00%> (-5.73%) ⬇️
cmd/daemon/main.go 0.00% <0.00%> (ø)
daemon/ccotel_server.go 0.00% <0.00%> (ø)
commands/cc.go 0.00% <0.00%> (ø)
model/api_ccotel.go 0.00% <0.00%> (ø)
model/ccotel_env.go 0.00% <0.00%> (ø)
daemon/ccotel_processor.go 0.00% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a passthrough OTEL collector for Claude Code telemetry, which is a significant feature. The implementation includes a new gRPC server, a data processor for OTEL metrics and logs, and corresponding API client logic. The code is generally well-structured and follows good practices. My review focuses on a potential issue with session timestamping that could impact analytics, and opportunities to improve performance and maintainability in the data processing logic.

Comment on lines +159 to +161
session := &model.CCOtelSession{
StartedAt: time.Now().Unix(),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The StartedAt timestamp for a session is set to time.Now().Unix() every time a batch of telemetry is received. For a single, long-running user session, this will result in multiple different StartedAt values being sent to the backend for the same session.id. This will make it impossible to accurately calculate session durations.

The session start time should be determined once when the session begins and then passed along with all telemetry for that session. Ideally, the client should include this as a resource attribute (e.g., session.started_at), which can then be parsed here.

Comment on lines +44 to +93
func (p *CCOtelProcessor) ProcessMetrics(ctx context.Context, req *collmetricsv1.ExportMetricsServiceRequest) (*collmetricsv1.ExportMetricsServiceResponse, error) {
slog.Debug("CCOtel: Processing metrics request", "resourceMetricsCount", len(req.GetResourceMetrics()))

for _, rm := range req.GetResourceMetrics() {
resource := rm.GetResource()

// Check if this is from Claude Code
if !isClaudeCodeResource(resource) {
slog.Debug("CCOtel: Skipping non-Claude Code resource")
continue
}

session := extractSessionFromResource(resource)
project := p.detectProject(resource)

var metrics []model.CCOtelMetric

for _, sm := range rm.GetScopeMetrics() {
for _, m := range sm.GetMetrics() {
parsedMetrics := p.parseMetric(m)
metrics = append(metrics, parsedMetrics...)
}
}

if len(metrics) == 0 {
continue
}

// Build and send request immediately
ccReq := &model.CCOtelRequest{
Host: p.hostname,
Project: project,
Session: session,
Metrics: metrics,
}

resp, err := model.SendCCOtelData(ctx, ccReq, p.endpoint)
if err != nil {
slog.Error("CCOtel: Failed to send metrics to backend", "error", err)
// Continue processing - passthrough mode, we don't retry
} else {
slog.Debug("CCOtel: Metrics sent to backend", "metricsProcessed", resp.MetricsProcessed)
}
}

return &collmetricsv1.ExportMetricsServiceResponse{}, nil
}

// ProcessLogs receives OTEL logs/events and forwards to backend immediately
func (p *CCOtelProcessor) ProcessLogs(ctx context.Context, req *collogsv1.ExportLogsServiceRequest) (*collogsv1.ExportLogsServiceResponse, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is significant code duplication between ProcessMetrics and ProcessLogs. The overall structure is identical:

  • Iterate over resources in the request.
  • Check if it's a Claude Code resource.
  • Extract session and project information.
  • Loop through scopes and data points/log records to parse them.
  • Build and send a request to the backend.

This duplication makes the code harder to maintain, as any changes to the common logic will need to be applied in both functions. Consider refactoring the common parts into a generic helper function to improve maintainability.

Comment on lines +59 to +66
var metrics []model.CCOtelMetric

for _, sm := range rm.GetScopeMetrics() {
for _, m := range sm.GetMetrics() {
parsedMetrics := p.parseMetric(m)
metrics = append(metrics, parsedMetrics...)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The metrics slice is grown by appending inside nested loops. For requests with a large number of metrics, this can lead to multiple slice reallocations, impacting performance. Since this is part of a telemetry pipeline, optimizing for throughput is important.

You can improve this by pre-calculating the total number of metrics within the ResourceMetrics and pre-allocating the slice with the required capacity.

Suggested change
var metrics []model.CCOtelMetric
for _, sm := range rm.GetScopeMetrics() {
for _, m := range sm.GetMetrics() {
parsedMetrics := p.parseMetric(m)
metrics = append(metrics, parsedMetrics...)
}
}
capacity := 0
for _, sm := range rm.GetScopeMetrics() {
capacity += len(sm.GetMetrics())
}
metrics := make([]model.CCOtelMetric, 0, capacity)
for _, sm := range rm.GetScopeMetrics() {
for _, m := range sm.GetMetrics() {
parsedMetrics := p.parseMetric(m)
metrics = append(metrics, parsedMetrics...)
}
}

Comment on lines +108 to +117
var events []model.CCOtelEvent

for _, sl := range rl.GetScopeLogs() {
for _, lr := range sl.GetLogRecords() {
event := p.parseLogRecord(lr)
if event != nil {
events = append(events, *event)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to ProcessMetrics, the events slice is grown by appending in a loop, which can be inefficient for large numbers of log records. Pre-allocating the slice with an estimated capacity will improve performance and reduce memory allocations.

Suggested change
var events []model.CCOtelEvent
for _, sl := range rl.GetScopeLogs() {
for _, lr := range sl.GetLogRecords() {
event := p.parseLogRecord(lr)
if event != nil {
events = append(events, *event)
}
}
}
capacity := 0
for _, sl := range rl.GetScopeLogs() {
capacity += len(sl.GetLogRecords())
}
events := make([]model.CCOtelEvent, 0, capacity)
for _, sl := range rl.GetScopeLogs() {
for _, lr := range sl.GetLogRecords() {
event := p.parseLogRecord(lr)
if event != nil {
events = append(events, *event)
}
}
}

AnnatarHe and others added 3 commits December 14, 2025 00:15
Add `shelltime cc install` command that automatically configures
Claude Code OTEL environment variables in shell config files.

- Add CCOtelEnvService interface with bash/zsh/fish implementations
- Support markers for clean install/uninstall
- Auto-detect and install for all supported shells
- Aliases: `cc i` for install, `cc u` for uninstall

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove backup functionality for simpler direct writes
- Extract OTEL endpoint to constant for easy modification
- Always remove existing env blocks before installing fresh config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move socket path configuration from daemon-specific config to the main
ShellTimeConfig. This simplifies the architecture by having a single
source of truth for configuration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@AnnatarHe AnnatarHe merged commit 7b4f728 into main Dec 14, 2025
2 of 3 checks passed
@AnnatarHe AnnatarHe deleted the feat/ccotel-otel-v2-passthrough branch December 14, 2025 16:53
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