Skip to content

Commit bb00d6c

Browse files
committed
feat: add custom OpenTelemetry collector endpoints support
- Add OpenTelemetryClient class to packages/telemetry for sending traces to custom OTEL endpoints - Add OTEL configuration fields (otelEnabled, otelEndpoints) to global settings schema - Create UI components in settings view for managing OTEL endpoints with add/remove/edit capabilities - Integrate OTEL client initialization in extension startup - Add message handlers for OTEL settings updates between webview and extension - Include comprehensive test coverage for OpenTelemetryClient - Support multiple endpoints with custom headers and enable/disable toggles per endpoint Implements #7020
1 parent 245bc2a commit bb00d6c

File tree

13 files changed

+1467
-8
lines changed

13 files changed

+1467
-8
lines changed

packages/telemetry/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
"clean": "rimraf dist .turbo"
1212
},
1313
"dependencies": {
14+
"@opentelemetry/api": "^1.9.0",
15+
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
16+
"@opentelemetry/instrumentation": "^0.56.0",
17+
"@opentelemetry/resources": "^1.29.0",
18+
"@opentelemetry/sdk-trace-base": "^1.29.0",
19+
"@opentelemetry/sdk-trace-node": "^1.29.0",
20+
"@opentelemetry/semantic-conventions": "^1.29.0",
1421
"@roo-code/types": "workspace:^",
1522
"posthog-node": "^5.0.0",
1623
"zod": "^3.25.61"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { trace, SpanStatusCode, Tracer } from "@opentelemetry/api"
2+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
3+
import { Resource } from "@opentelemetry/resources"
4+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
5+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
6+
import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base"
7+
import { registerInstrumentations } from "@opentelemetry/instrumentation"
8+
9+
import { type TelemetryEvent } from "@roo-code/types"
10+
11+
import { BaseTelemetryClient } from "./BaseTelemetryClient"
12+
13+
// Conditionally import vscode only when not in test environment
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
let vscode: any
16+
try {
17+
// eslint-disable-next-line @typescript-eslint/no-require-imports
18+
vscode = require("vscode")
19+
} catch {
20+
// In test environment, vscode is not available
21+
vscode = {
22+
extensions: {
23+
getExtension: () => ({ packageJSON: { version: "test-version" } }),
24+
},
25+
workspace: {
26+
getConfiguration: () => ({
27+
get: () => "all",
28+
}),
29+
},
30+
}
31+
}
32+
33+
export interface OtelEndpoint {
34+
url: string
35+
headers?: Record<string, string>
36+
enabled: boolean
37+
}
38+
39+
/**
40+
* OpenTelemetryClient handles telemetry event tracking using OpenTelemetry.
41+
* Supports sending traces to multiple custom endpoints in addition to internal endpoints.
42+
* Respects user privacy settings and VSCode's global telemetry configuration.
43+
*/
44+
export class OpenTelemetryClient extends BaseTelemetryClient {
45+
private tracer: Tracer | null = null
46+
private provider: NodeTracerProvider | null = null
47+
private endpoints: OtelEndpoint[] = []
48+
private isInitialized = false
49+
50+
constructor(debug = false) {
51+
super(undefined, debug)
52+
}
53+
54+
/**
55+
* Initialize or reinitialize the OpenTelemetry provider with the given endpoints
56+
* @param endpoints Array of OTEL collector endpoints to send traces to
57+
*/
58+
public async initialize(endpoints: OtelEndpoint[]): Promise<void> {
59+
try {
60+
// Shutdown existing provider if any
61+
if (this.provider) {
62+
await this.shutdown()
63+
}
64+
65+
this.endpoints = endpoints.filter((ep) => ep.enabled)
66+
67+
// Create resource with service information
68+
const version =
69+
vscode?.extensions?.getExtension?.("rooveterinaryinc.roo-cline")?.packageJSON?.version || "unknown"
70+
const resource = new Resource({
71+
[ATTR_SERVICE_NAME]: "roo-code",
72+
[ATTR_SERVICE_VERSION]: version,
73+
})
74+
75+
// Create provider
76+
this.provider = new NodeTracerProvider({
77+
resource,
78+
})
79+
80+
// Add exporters for each endpoint
81+
for (const endpoint of this.endpoints) {
82+
const exporter = new OTLPTraceExporter({
83+
url: endpoint.url,
84+
headers: endpoint.headers || {},
85+
})
86+
87+
// Use BatchSpanProcessor for better performance
88+
this.provider.addSpanProcessor(new BatchSpanProcessor(exporter))
89+
}
90+
91+
// Add console exporter in debug mode
92+
if (this.debug) {
93+
this.provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter()))
94+
}
95+
96+
// Register the provider
97+
this.provider.register()
98+
99+
// Register instrumentations
100+
registerInstrumentations({
101+
instrumentations: [],
102+
})
103+
104+
// Only get tracer if we have endpoints
105+
if (this.endpoints.length > 0) {
106+
// Get tracer with version
107+
this.tracer = trace.getTracer("roo-code-telemetry", version)
108+
this.isInitialized = true
109+
}
110+
111+
if (this.debug) {
112+
console.info(`[OpenTelemetryClient#initialize] Initialized with ${this.endpoints.length} endpoints`)
113+
}
114+
} catch (error) {
115+
console.error("[OpenTelemetry] Failed to initialize:", error)
116+
// Don't throw - just log the error
117+
}
118+
}
119+
120+
/**
121+
* Update endpoints configuration
122+
* This will reinitialize the provider with the new endpoints
123+
* @param endpoints New array of OTEL collector endpoints
124+
*/
125+
public async updateEndpoints(endpoints: OtelEndpoint[]): Promise<void> {
126+
await this.initialize(endpoints)
127+
}
128+
129+
public override async capture(event: TelemetryEvent): Promise<void> {
130+
if (!this.isTelemetryEnabled() || !this.isInitialized || !this.tracer) {
131+
if (this.debug) {
132+
console.info(
133+
`[OpenTelemetryClient#capture] Skipping event: ${event.event} (enabled: ${this.isTelemetryEnabled()}, initialized: ${this.isInitialized})`,
134+
)
135+
}
136+
return
137+
}
138+
139+
try {
140+
if (this.debug) {
141+
console.info(`[OpenTelemetryClient#capture] ${event.event}`)
142+
}
143+
144+
// Get event properties
145+
const properties = await this.getEventProperties(event)
146+
147+
// Create a span for the event
148+
const span = this.tracer.startSpan(event.event)
149+
150+
// Set attributes after creating the span
151+
if (properties && Object.keys(properties).length > 0) {
152+
span.setAttributes(properties)
153+
}
154+
155+
// Set span status to OK and end it immediately since these are point-in-time events
156+
span.setStatus({ code: SpanStatusCode.OK })
157+
span.end()
158+
} catch (error) {
159+
console.error("[OpenTelemetry] Failed to capture event:", error)
160+
// Don't throw - just log the error
161+
}
162+
}
163+
164+
/**
165+
* Updates the telemetry state based on user preferences and VSCode settings.
166+
* Only enables telemetry if both VSCode global telemetry is enabled and
167+
* user has opted in.
168+
* @param didUserOptIn Whether the user has explicitly opted into telemetry
169+
*/
170+
public override updateTelemetryState(didUserOptIn: boolean): void {
171+
this.telemetryEnabled = false
172+
173+
// First check global telemetry level - telemetry should only be enabled when level is "all".
174+
const telemetryLevel =
175+
vscode?.workspace?.getConfiguration?.("telemetry")?.get?.("telemetryLevel", "all") || "all"
176+
const globalTelemetryEnabled = telemetryLevel === "all"
177+
178+
// We only enable telemetry if global vscode telemetry is enabled.
179+
if (globalTelemetryEnabled) {
180+
this.telemetryEnabled = didUserOptIn
181+
}
182+
183+
if (this.debug) {
184+
console.info(`[OpenTelemetryClient#updateTelemetryState] Telemetry enabled: ${this.telemetryEnabled}`)
185+
}
186+
}
187+
188+
public override async shutdown(): Promise<void> {
189+
if (this.provider) {
190+
try {
191+
await this.provider.shutdown()
192+
} catch (error) {
193+
console.error("[OpenTelemetry] Failed to shutdown:", error)
194+
// Don't throw - just log the error
195+
}
196+
this.provider = null
197+
this.tracer = null
198+
this.isInitialized = false
199+
}
200+
}
201+
202+
/**
203+
* Get the currently configured endpoints
204+
* @returns Array of configured OTEL endpoints
205+
*/
206+
public getEndpoints(): OtelEndpoint[] {
207+
return [...this.endpoints]
208+
}
209+
}

0 commit comments

Comments
 (0)