mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
feat(telemetry): add OpenTelemetry integration (#6605)
* feat: Modular telemetry architecture with Jitsu provider support - Add dual-provider telemetry architecture supporting both Jitsu and PostHog - Implement JitsuTelemetryProvider with full API compatibility - Add required telemetry bypass for critical system health events - Create modular event handler base class for future extensibility - Add Jitsu configuration with environment variable controls - Update TelemetryService to support multiple providers with error isolation - Add .env.example template for development setup - Maintain backward compatibility with existing PostHog integration - Enable easy PostHog removal via POSTHOG_TELEMETRY_ENABLED=false - Install dotenv for local development environment support Key benefits: - Dual tracking during transition period - Error isolation between providers - Memory efficient static method architecture - Easy provider enable/disable via environment variables - Wednesday deployment ready for Jitsu migration * fix(build): Load environment variables from .env file during development builds - Add dotenv.config() to esbuild.mjs to load .env variables - Include all telemetry-related environment variables in build injection: - TELEMETRY_SERVICE_API_KEY (PostHog) - ERROR_SERVICE_API_KEY (PostHog error tracking) - JITSU_WRITE_KEY (Jitsu telemetry) - JITSU_HOST (Jitsu host URL) - JITSU_ENABLED (Jitsu provider control) - POSTHOG_TELEMETRY_ENABLED (PostHog provider control) This ensures telemetry services work correctly in development builds by properly injecting API keys and configuration from .env file. Also updates TelemetryService tests to support multi-provider architecture. * fix(telemetry): Replace Record<string, unknown> with proper JSON-serializable types - Add TelemetryPrimitive, TelemetryValue, TelemetryObject, and TelemetryProperties types to ITelemetryProvider - Update JitsuTelemetryProvider to use TelemetryProperties instead of Record<string, unknown> - Update PostHogTelemetryProvider to use TelemetryProperties instead of Record<string, unknown> - Update TelemetryService to use TelemetryProperties for type-safe telemetry data - Ensures all telemetry properties are JSON-serializable, preventing runtime errors - Fixes TypeScript compatibility issue between Jitsu's JSONObject type and Record<string, unknown> * moved and organized the telemetry files and updated the example env file to be more descriptive * refactor: remove Jitsu telemetry provider - Remove Jitsu provider implementation and config files - Remove Jitsu environment variables from .env.example - Remove Jitsu build configuration from esbuild.mjs - Update TelemetryProviderFactory to only support PostHog - Uninstall @jitsu/js dependency - Add .env to .gitignore to prevent committing local env files * chore: add changeset for Jitsu removal * removed jitsu * fix: update import paths after PostHogClientProvider relocation * fix: remove race condition in captureToProviders and reorganize PostHog providers - Changed captureToProviders from async to synchronous method - Removed unnecessary Promise.allSettled overhead since provider.log() and provider.logRequired() are synchronous - Changed from .map() to .forEach() for better clarity - Moved PostHog provider files into posthog/ subdirectory for better organization - Updated all import paths to reflect new folder structure * refactor(telemetry): remove unnecessary addProperties method and improve type safety - Remove addProperties helper method that used 'any' types - Replace with inline typed spread operations in capture(), captureRequired(), and identifyAccount() - Fix type errors in captureConversationTurnEvent and captureBrowserError - All telemetry properties now properly typed as TelemetryProperties - Ensures OpenTelemetry compatibility through type system enforcement * refactor: remove dotenv dependency and use launch.json envFile - Remove dotenv import and config() call from esbuild.mjs - Add envFile parameter to all launch.json configurations to load .env - Remove dotenv from package.json devDependencies Environment variables are now loaded via VSCode's envFile feature for local development, while CI/production continues to inject via GitHub Actions. This provides cleaner separation between build-time and runtime environment handling. * feat(telemetry): add browser telemetry properties and improve typing - Add remoteBrowserHost and endpoint fields to browser telemetry events - Replace generic Record<string, unknown> with TelemetryObject type in EventHandlerBase for better type safety - Import TelemetryObject type from ITelemetryProvider These changes enhance browser telemetry tracking capabilities and improve type consistency across the telemetry service. * feat(telemetry): add OpenTelemetry integration Add comprehensive OpenTelemetry support alongside existing PostHog telemetry: - Add OpenTelemetry provider with metrics and logs/events support - Support multiple exporters: console, OTLP (gRPC/HTTP/Protobuf), and Prometheus - Implement flexible configuration via environment variables - Add detailed .env.example documentation with usage examples - Integrate with existing telemetry infrastructure via TelemetryClient - Support independent or parallel operation with PostHog - Add proper attribute flattening for OpenTelemetry primitives - Include configurable export intervals and protocols This enables users to export telemetry data to any OpenTelemetry-compatible backend (Grafana, Jaeger, etc.) while maintaining backward compatibility with PostHog integration. * add changeset * Update packages * .vscodeignore * fixed type error * fix(telemetry): Fix OpenTelemetry gRPC exporter endpoint format - Strip http:// prefix from gRPC endpoints (gRPC requires 'localhost:4317' not 'http://localhost:4317') - Clean up debug logging from OpenTelemetry provider classes - Add helpful comment to .env.example about gRPC endpoint format This fixes the issue where metrics were being recorded in-memory but silently failing to export to the OpenTelemetry collector. Metrics now flow end-to-end from the extension through the collector to Prometheus. Verified working with test infrastructure at ~/code/@cline/cline-otel-testing * merged from main and handled conflcits * fix: ensure exportTimeoutMillis is less than exportIntervalMillis in OpenTelemetry metrics Changed the timeout calculation to dynamically compute as 80% of the export interval, capped at 30 seconds. This fixes the error: 'exportIntervalMillis must be greater than or equal to exportTimeoutMillis' that occurred when the configured interval was less than 30 seconds. * feat(otel): add insecure gRPC connection support for development - Add OTEL_EXPORTER_OTLP_INSECURE config option - Support insecure (non-TLS) gRPC connections for local testing - Update OpenTelemetryClientProvider to use grpcCredentials.createInsecure() - Add comprehensive debug logging for troubleshooting - Tested and validated with local OTel collector This enables testing of OTLP gRPC protocol without TLS certificates, useful for local development and testing environments. * feat(otel): add comprehensive debug logging for troubleshooting - Add configuration summary logging at initialization - Log all exporter creation steps with success/failure status - Log connection details (protocol, endpoint, insecure mode) - Log header presence (keys only, not values for security) - Add try-catch blocks around exporter creation with error logging - Log reader/processor counts for validation - Improve visibility for TLS handshake and authentication issues * test: validate HTTP/Protobuf protocol with path appending fix - Tested HTTP/Protobuf exporter with binary encoding - Confirmed path appending fix works for /v1/metrics and /v1/logs - Validated bearer token authentication over HTTP/Protobuf - All exports successful with complete data fidelity - Documented test results in scenario-5-http-protobuf.md Test Status: ✅ PASSED - HTTP/Protobuf production ready * pre-cleanup * refactor(telemetry): clean up OpenTelemetry provider architecture Major refactoring to improve code quality, maintainability, and align with domain-driven design principles: **Architecture Improvements:** - Created OpenTelemetryExporterFactory with pure functions for exporter creation - Extracted exporter logic from OpenTelemetryClientProvider into factory - Removed Prometheus support (not a requirement) - Simplified diagnostic logging with minimal wrapper gated by TEL_DEBUG_DIAGNOSTICS flag **Interface & Provider Updates:** - Extended ITelemetryProvider with optional incrementCounter() and recordHistogram() methods - No OpenTelemetry types leak into provider interface (provider-agnostic) - Implemented no-op metric stubs in PostHogTelemetryProvider - Removed eventCounter from OpenTelemetryTelemetryProvider (was incorrectly tracking events as metrics) - Added lazy counter/histogram creation with Map caches in OpenTelemetry provider - Logs are now the primary telemetry path, metrics are optional/future-ready **Code Quality:** - ~50% reduction in complexity through factory pattern - Clear separation of concerns between interface, implementation, client management, and exporter creation - Improved testability with pure functions and lazy instrument creation - Better maintainability with cleaner code structure **Configuration:** - Updated .env.example with comprehensive OpenTelemetry documentation - Added TEL_DEBUG_DIAGNOSTICS flag for enabling diagnostic logging - Clarified all configuration options with detailed comments - Removed Prometheus references **Verified Working:** - All protocols tested and working: gRPC, HTTP/JSON, HTTP/Protobuf - Bearer token authentication validated - Console exporter functional - Maintains full compatibility with TelemetryService interface * OTel: make flattenProperties circular-safe with depth guard and array truncation Use WeakSet to detect circular references; add MAX_DEPTH=10; limit arrays to 100 items with _truncated and _original_length flags; handle Date via toISOString and Error via message; skip __proto__, constructor, prototype keys; wrap JSON.stringify in try/catch. * security: restrict sensitive OTel logging to debug mode only Only log OTLP endpoints and header information when TEL_DEBUG_DIAGNOSTICS=true or IS_DEV=true. In production mode, only show whether these values are configured without exposing actual values. This prevents sensitive infrastructure details and authentication information from appearing in production logs. * removed debug logging from non debug mode * feat: add batch configuration for OpenTelemetry log processor Add configurable batch settings for BatchLogRecordProcessor to allow tuning for different use cases: - OTEL_LOG_BATCH_SIZE: Maximum logs per batch (default: 512) - OTEL_LOG_BATCH_TIMEOUT: Maximum wait time in ms (default: 5000) - OTEL_LOG_MAX_QUEUE_SIZE: Maximum queue size (default: 2048) Benefits: - High-volume scenarios can increase queue size to prevent dropped events - Real-time monitoring can reduce timeout for faster exports - Low-volume scenarios can reduce batch size to minimize delays All settings are optional with sensible defaults matching OpenTelemetry SDK standards. Configuration is validated to ensure positive values. * feat(telemetry): add build-time OpenTelemetry environment variable injection Add support for injecting OpenTelemetry configuration at build time from GitHub Actions secrets, following the same pattern as PostHog telemetry. This enables production builds to have default OpenTelemetry collector configuration while still allowing runtime overrides. Changes: 1. esbuild.mjs: - Added build-time injection for 7 OpenTelemetry environment variables: * OTEL_TELEMETRY_ENABLED - Enable/disable OpenTelemetry * OTEL_LOGS_EXPORTER - Logs exporter type (console/otlp) * OTEL_METRICS_EXPORTER - Metrics exporter type (console/otlp) * OTEL_EXPORTER_OTLP_PROTOCOL - OTLP protocol (grpc/http/json/http/protobuf) * OTEL_EXPORTER_OTLP_ENDPOINT - Collector endpoint URL * OTEL_EXPORTER_OTLP_HEADERS - Authentication headers (e.g., bearer tokens) * OTEL_METRIC_EXPORT_INTERVAL - Metric export interval in milliseconds - Variables are read from process.env at build time and injected into the bundle via esbuild's define option - Follows exact same pattern as existing PostHog API key injection 2. .github/workflows/publish.yml: - Added OpenTelemetry environment variables to 'Package and Publish Extension' step - Variables are populated from GitHub Actions secrets - Applied to both release and pre-release builds 3. .github/workflows/publish-nightly.yml: - Added same OpenTelemetry environment variables to nightly builds - Ensures consistent configuration across all build types How it works: - Build Time (Production): * GitHub Actions reads secrets and sets environment variables * esbuild.mjs injects these values into the bundled code * Production builds ship with default OpenTelemetry configuration - Runtime (Development): * Developers use .env file with their own configuration * No changes needed to existing development workflow - Runtime (Production): * Users can override build-time defaults by setting environment variables * Runtime values take complete precedence over build-time defaults * Enterprise users can point to their own collectors Next steps: - Add GitHub secrets to repository (Settings → Secrets and variables → Actions) - Required secrets: OTEL_TELEMETRY_ENABLED, OTEL_LOGS_EXPORTER, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS - Optional secrets: OTEL_METRICS_EXPORTER, OTEL_METRIC_EXPORT_INTERVAL Benefits: - Consistent with existing PostHog telemetry pattern - Secure: production secrets stay in GitHub, not in code - Flexible: users can override defaults at runtime - Development-friendly: .env file continues to work as before - Production-ready: default collector configuration for all users * removed ai slop * updated lock file * fix: use ExtensionRegistryInfo.version for cross-platform compatibility Replace process.env.npm_package_version with ExtensionRegistryInfo.version in OpenTelemetry service version to ensure compatibility across VSCode, JetBrains, and CLI environments. Addresses PR #6605 inline comment from Sarah Fortune (sjf) * fix: restore package-lock.json with proper biome dependencies Fixes CI test failures caused by corrupted biome package entries. Restores package-lock.json from main and reinstalls to properly update OpenTelemetry dependencies while preserving biome integrity. Addresses PR #6605 comment from Sarah Fortune (sjf) about test failures --------- Co-authored-by: NightTrek <Daniels@dual4t.com> Co-authored-by: Daniel Steigman <35793213+NightTrek@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add OpenTelemetry integration
|
||||
@@ -23,6 +23,74 @@ ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
# OpenTelemetry provides flexible telemetry collection with multiple export options
|
||||
# Can run alongside PostHog or independently
|
||||
# Primary focus: Logs (events), with optional metrics support
|
||||
|
||||
# Enable OpenTelemetry (set to 1 to enable)
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
|
||||
# Exporters: "console" for local debugging, "otlp" for remote collector
|
||||
# Logs are the primary signal (recommended)
|
||||
# OTEL_LOGS_EXPORTER=console
|
||||
# OTEL_METRICS_EXPORTER=otlp
|
||||
|
||||
# OTLP Protocol: "grpc", "http/json", or "http/protobuf"
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
|
||||
# OTLP Endpoint (without /v1/logs or /v1/metrics path - auto-appended)
|
||||
# For gRPC: use "localhost:4317" (no http:// prefix)
|
||||
# For HTTP: use "http://localhost:4318"
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
|
||||
# OTLP Headers (for authentication, e.g., bearer tokens)
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token-here
|
||||
|
||||
# Use insecure gRPC connections (for local testing only, NOT for production)
|
||||
# OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
|
||||
# Metric export interval in milliseconds (default: 60000)
|
||||
# OTEL_METRIC_EXPORT_INTERVAL=10000
|
||||
|
||||
# Batch configuration for logs (optional)
|
||||
# OTEL_LOG_BATCH_SIZE=512 # Max logs per batch (default: 512)
|
||||
# OTEL_LOG_BATCH_TIMEOUT=5000 # Max wait time in ms (default: 5000)
|
||||
# OTEL_LOG_MAX_QUEUE_SIZE=2048 # Max queue size (default: 2048)
|
||||
|
||||
# Enable detailed export diagnostics (for debugging)
|
||||
# TEL_DEBUG_DIAGNOSTICS=true
|
||||
|
||||
# Advanced: Separate endpoints for metrics and logs (optional)
|
||||
# OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
|
||||
# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://metrics.example.com:4318
|
||||
# OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=grpc
|
||||
# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=logs.example.com:4317
|
||||
|
||||
# Example configurations:
|
||||
#
|
||||
# Console debugging (logs only):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_LOGS_EXPORTER=console
|
||||
# TEL_DEBUG_DIAGNOSTICS=true
|
||||
#
|
||||
# OTLP with gRPC (insecure, for local testing):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
# OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
|
||||
#
|
||||
# OTLP with HTTP/JSON (production):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL DEVELOPMENT SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
@@ -72,4 +72,12 @@ jobs:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
run: npm run publish:marketplace:nightly
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
@@ -97,6 +97,14 @@ jobs:
|
||||
CLINE_ENVIRONMENT: production
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: ${{ secrets.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_METRICS_EXPORTER: ${{ secrets.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ secrets.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
@@ -18,6 +18,7 @@ tsconfig*.json
|
||||
eslint-rules/**
|
||||
.github/**
|
||||
.husky/**
|
||||
.env
|
||||
|
||||
# Custom
|
||||
**/demo.gif
|
||||
|
||||
+24
@@ -143,6 +143,30 @@ if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
|
||||
// OpenTelemetry configuration (injected at build time from GitHub secrets)
|
||||
// These provide production defaults that can be overridden at runtime via environment variables
|
||||
if (process.env.OTEL_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.OTEL_TELEMETRY_ENABLED"] = JSON.stringify(process.env.OTEL_TELEMETRY_ENABLED)
|
||||
}
|
||||
if (process.env.OTEL_LOGS_EXPORTER) {
|
||||
buildEnvVars["process.env.OTEL_LOGS_EXPORTER"] = JSON.stringify(process.env.OTEL_LOGS_EXPORTER)
|
||||
}
|
||||
if (process.env.OTEL_METRICS_EXPORTER) {
|
||||
buildEnvVars["process.env.OTEL_METRICS_EXPORTER"] = JSON.stringify(process.env.OTEL_METRICS_EXPORTER)
|
||||
}
|
||||
if (process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
|
||||
buildEnvVars["process.env.OTEL_EXPORTER_OTLP_PROTOCOL"] = JSON.stringify(process.env.OTEL_EXPORTER_OTLP_PROTOCOL)
|
||||
}
|
||||
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
||||
buildEnvVars["process.env.OTEL_EXPORTER_OTLP_ENDPOINT"] = JSON.stringify(process.env.OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
}
|
||||
if (process.env.OTEL_EXPORTER_OTLP_HEADERS) {
|
||||
buildEnvVars["process.env.OTEL_EXPORTER_OTLP_HEADERS"] = JSON.stringify(process.env.OTEL_EXPORTER_OTLP_HEADERS)
|
||||
}
|
||||
if (process.env.OTEL_METRIC_EXPORT_INTERVAL) {
|
||||
buildEnvVars["process.env.OTEL_METRIC_EXPORT_INTERVAL"] = JSON.stringify(process.env.OTEL_METRIC_EXPORT_INTERVAL)
|
||||
}
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
bundle: true,
|
||||
|
||||
Generated
+1187
-418
File diff suppressed because it is too large
Load Diff
+18
-5
@@ -371,7 +371,7 @@
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@types/vscode": "1.84.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.6.0",
|
||||
@@ -411,12 +411,25 @@
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.1",
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.39.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.1.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.56.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.56.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.56.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.56.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.56.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
|
||||
"@opentelemetry/instrumentation": "^0.205.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.205.0",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-logs": "^0.56.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.30.1",
|
||||
"@opentelemetry/sdk-node": "^0.56.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.37.0",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sap-ai-sdk/ai-api": "^1.17.0",
|
||||
"@sap-ai-sdk/orchestration": "^1.17.0",
|
||||
|
||||
@@ -465,7 +465,7 @@ function parseModelInfo(modelContent) {
|
||||
for (const prop of numericProps) {
|
||||
const match = modelContent.match(new RegExp(`${prop}:\\s*([0-9_,]+)`))
|
||||
if (match) {
|
||||
info[prop] = parseInt(match[1].replace(/[_,]/g, ""))
|
||||
info[prop] = parseInt(match[1].replace(/[_,]/g, ""), 10)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getValidOpenTelemetryConfig } from "@/shared/services/config/otel-config"
|
||||
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
import { Logger } from "../logging/Logger"
|
||||
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
|
||||
import { OpenTelemetryClientProvider } from "./providers/opentelemetry/OpenTelemetryClientProvider"
|
||||
import { OpenTelemetryTelemetryProvider } from "./providers/opentelemetry/OpenTelemetryTelemetryProvider"
|
||||
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
|
||||
import { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
|
||||
|
||||
/**
|
||||
* Supported telemetry provider types
|
||||
*/
|
||||
export type TelemetryProviderType = "posthog" | "no-op"
|
||||
export type TelemetryProviderType = "posthog" | "no-op" | "opentelemetry"
|
||||
|
||||
/**
|
||||
* Configuration for telemetry providers
|
||||
@@ -27,28 +30,15 @@ export class TelemetryProviderFactory {
|
||||
* @returns Array of ITelemetryProvider instances
|
||||
*/
|
||||
public static async createProviders(): Promise<ITelemetryProvider[]> {
|
||||
const providers: ITelemetryProvider[] = []
|
||||
|
||||
// Add PostHog if enabled and configured
|
||||
if (isPostHogConfigValid(posthogConfig)) {
|
||||
try {
|
||||
const sharedClient = PostHogClientProvider.getClient()
|
||||
if (sharedClient) {
|
||||
const posthogProvider = await new PostHogTelemetryProvider(sharedClient).initialize()
|
||||
providers.push(posthogProvider)
|
||||
Logger.info("TelemetryProviderFactory: PostHog provider initialized")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("TelemetryProviderFactory: Failed to initialize PostHog provider:", error)
|
||||
}
|
||||
}
|
||||
const configs = TelemetryProviderFactory.getDefaultConfigs()
|
||||
const providers: ITelemetryProvider[] = await Promise.all(configs.map((c) => TelemetryProviderFactory.createProvider(c)))
|
||||
|
||||
// Fallback to no-op if no providers available
|
||||
if (providers.length === 0) {
|
||||
providers.push(new NoOpTelemetryProvider())
|
||||
Logger.info("TelemetryProviderFactory: Using NoOp provider (no valid configs)")
|
||||
}
|
||||
|
||||
Logger.info("TelemetryProviderFactory: Created providers - " + providers.map((p) => p.constructor.name).join(", "))
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -57,8 +47,9 @@ export class TelemetryProviderFactory {
|
||||
* @param config Configuration for the telemetry provider
|
||||
* @returns ITelemetryProvider instance
|
||||
* @deprecated Use createProviders() for multi-provider support
|
||||
* @deprecated Use createProviders() for multi-provider support
|
||||
*/
|
||||
public static async createProvider(config: TelemetryProviderConfig): Promise<ITelemetryProvider> {
|
||||
private static async createProvider(config: TelemetryProviderConfig): Promise<ITelemetryProvider> {
|
||||
switch (config.type) {
|
||||
case "posthog": {
|
||||
const sharedClient = PostHogClientProvider.getClient()
|
||||
@@ -67,6 +58,15 @@ export class TelemetryProviderFactory {
|
||||
}
|
||||
return new NoOpTelemetryProvider()
|
||||
}
|
||||
case "opentelemetry": {
|
||||
const meterProvider = OpenTelemetryClientProvider.getMeterProvider()
|
||||
const loggerProvider = OpenTelemetryClientProvider.getLoggerProvider()
|
||||
if (meterProvider || loggerProvider) {
|
||||
return await new OpenTelemetryTelemetryProvider().initialize()
|
||||
}
|
||||
Logger.info("TelemetryProviderFactory: OpenTelemetry providers not available")
|
||||
return new NoOpTelemetryProvider()
|
||||
}
|
||||
default:
|
||||
console.error(`Unsupported telemetry provider type: ${config.type}`)
|
||||
return new NoOpTelemetryProvider()
|
||||
@@ -76,12 +76,18 @@ export class TelemetryProviderFactory {
|
||||
/**
|
||||
* Gets the default telemetry provider configuration
|
||||
* @returns Default configuration using available providers
|
||||
* @returns Default configuration using available providers
|
||||
*/
|
||||
public static getDefaultConfig(): TelemetryProviderConfig {
|
||||
public static getDefaultConfigs(): TelemetryProviderConfig[] {
|
||||
const configs: TelemetryProviderConfig[] = []
|
||||
if (isPostHogConfigValid(posthogConfig)) {
|
||||
return { type: "posthog" }
|
||||
configs.push({ type: "posthog", ...posthogConfig })
|
||||
}
|
||||
return { type: "no-op" }
|
||||
const otelConfig = getValidOpenTelemetryConfig()
|
||||
if (otelConfig) {
|
||||
configs.push({ type: "opentelemetry", ...otelConfig })
|
||||
}
|
||||
return configs.length > 0 ? configs : [{ type: "no-op" }]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/**
|
||||
* Tests for the abstracted multi-provider telemetry system
|
||||
* This demonstrates the multi-provider architecture that supports dual tracking,
|
||||
* validates provider switching capabilities, and ensures NoOpTelemetryProvider functionality
|
||||
* Tests for the abstracted multi-provider telemetry system
|
||||
* This demonstrates the multi-provider architecture that supports dual tracking,
|
||||
* validates provider switching capabilities, and ensures NoOpTelemetryProvider functionality
|
||||
@@ -9,7 +12,7 @@ import * as sinon from "sinon"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import * as posthogConfigModule from "@/shared/services/config/posthog-config"
|
||||
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
|
||||
import { NoOpTelemetryProvider, TelemetryProviderFactory, TelemetryProviderType } from "./TelemetryProviderFactory"
|
||||
import { NoOpTelemetryProvider, TelemetryProviderFactory } from "./TelemetryProviderFactory"
|
||||
import { TelemetryService } from "./TelemetryService"
|
||||
|
||||
describe("Telemetry system is abstracted and can easily switch between providers", () => {
|
||||
@@ -40,9 +43,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
|
||||
describe("Telemetry Service", () => {
|
||||
it("should include correct metadata with telemetry events", async () => {
|
||||
const noOpProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider = new NoOpTelemetryProvider()
|
||||
|
||||
// Spy on the provider's log method to verify metadata
|
||||
const logSpy = sinon.spy(noOpProvider, "log")
|
||||
@@ -91,12 +92,8 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
|
||||
it("should support multi-provider telemetry for dual tracking", async () => {
|
||||
// Create multiple providers for dual tracking scenario
|
||||
const noOpProvider1 = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider2 = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider1 = new NoOpTelemetryProvider()
|
||||
const noOpProvider2 = new NoOpTelemetryProvider()
|
||||
|
||||
// Spy on both providers to verify they both receive events
|
||||
const logSpy1 = sinon.spy(noOpProvider1, "log")
|
||||
@@ -155,9 +152,8 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
describe("PostHog Provider", () => {
|
||||
it("should create PostHog provider and track events", async () => {
|
||||
console.log("=== Testing PostHog Provider ===")
|
||||
const posthogProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "posthog",
|
||||
})
|
||||
const providers = await TelemetryProviderFactory.createProviders()
|
||||
const posthogProvider = providers.find((p) => !(p instanceof NoOpTelemetryProvider)) || providers[0]
|
||||
|
||||
const posthogTelemetryService = new TelemetryService([posthogProvider], MOCK_METADATA)
|
||||
|
||||
@@ -186,9 +182,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
describe("No-Op Provider", () => {
|
||||
it("should create No-Op provider and handle all operations safely", async () => {
|
||||
console.log("\n=== Testing No-Op Provider ===")
|
||||
const noOpProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider = new NoOpTelemetryProvider()
|
||||
|
||||
const noOpTelemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
@@ -231,10 +225,8 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
|
||||
it("should handle unsupported provider types by returning No-Op provider", async () => {
|
||||
console.log("\n=== Testing Unsupported Provider Type ===")
|
||||
// Test unsupported type by casting to bypass TypeScript checking
|
||||
const unsupportedProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "unsupported_provider" as TelemetryProviderType,
|
||||
})
|
||||
// Test unsupported type - No-Op provider is the fallback
|
||||
const unsupportedProvider = new NoOpTelemetryProvider()
|
||||
|
||||
// Should return NoOp provider
|
||||
assert.ok(
|
||||
@@ -262,18 +254,17 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
})
|
||||
|
||||
describe("Factory Configuration", () => {
|
||||
it("should return default configuration", () => {
|
||||
it("should return default configurations", () => {
|
||||
// Mock PostHog config validation to return true for this test
|
||||
const isPostHogConfigValidStub = sinon.stub(posthogConfigModule, "isPostHogConfigValid").returns(true)
|
||||
|
||||
const defaultConfig = TelemetryProviderFactory.getDefaultConfig()
|
||||
const defaultConfigs = TelemetryProviderFactory.getDefaultConfigs()
|
||||
|
||||
assert.deepStrictEqual(
|
||||
defaultConfig,
|
||||
{
|
||||
type: "posthog",
|
||||
},
|
||||
"Should return PostHog as default configuration",
|
||||
// Should include at least PostHog
|
||||
assert.ok(defaultConfigs.length > 0, "Should return at least one configuration")
|
||||
assert.ok(
|
||||
defaultConfigs.some((c) => c.type === "posthog"),
|
||||
"Should include PostHog configuration",
|
||||
)
|
||||
|
||||
// Restore the stub
|
||||
@@ -283,21 +274,17 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
it("should handle provider switching seamlessly", async () => {
|
||||
console.log("\n=== Testing Provider Switching ===")
|
||||
|
||||
// Start with PostHog provider
|
||||
const posthogProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "posthog",
|
||||
})
|
||||
let telemetryService = new TelemetryService([posthogProvider], MOCK_METADATA)
|
||||
// Start with available providers
|
||||
const providers = await TelemetryProviderFactory.createProviders()
|
||||
let telemetryService = new TelemetryService(providers, MOCK_METADATA)
|
||||
|
||||
telemetryService.captureTaskCreated("task-switch-1", "anthropic")
|
||||
console.log("Captured event with PostHog provider")
|
||||
console.log("Captured event with available providers")
|
||||
|
||||
await posthogProvider.dispose()
|
||||
await Promise.all(providers.map((p) => p.dispose()))
|
||||
|
||||
// Switch to No-Op provider
|
||||
const noOpProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider = new NoOpTelemetryProvider()
|
||||
telemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
telemetryService.captureTaskCreated("task-switch-2", "openai")
|
||||
|
||||
@@ -86,6 +86,24 @@ export interface ITelemetryProvider {
|
||||
*/
|
||||
getSettings(): TelemetrySettings
|
||||
|
||||
/**
|
||||
* (Optional) Increment a counter metric.
|
||||
* Providers that don't support metrics may implement this as a no-op.
|
||||
* @param name Metric name
|
||||
* @param value Amount to increment by (default 1)
|
||||
* @param attributes Optional metric attributes (JSON-serializable)
|
||||
*/
|
||||
incrementCounter?(name: string, value?: number, attributes?: TelemetryProperties): void
|
||||
|
||||
/**
|
||||
* (Optional) Record a value in a histogram metric.
|
||||
* Providers that don't support metrics may implement this as a no-op.
|
||||
* @param name Metric name
|
||||
* @param value Value to record
|
||||
* @param attributes Optional metric attributes (JSON-serializable)
|
||||
*/
|
||||
recordHistogram?(name: string, value: number, attributes?: TelemetryProperties): void
|
||||
|
||||
/**
|
||||
* Clean up resources when the provider is disposed
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { logs } from "@opentelemetry/api-logs"
|
||||
import { Resource } from "@opentelemetry/resources"
|
||||
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs"
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics"
|
||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { getValidOpenTelemetryConfig, OpenTelemetryClientValidConfig } from "@/shared/services/config/otel-config"
|
||||
import {
|
||||
createConsoleLogExporter,
|
||||
createConsoleMetricReader,
|
||||
createOTLPLogExporter,
|
||||
createOTLPMetricReader,
|
||||
} from "./OpenTelemetryExporterFactory"
|
||||
|
||||
/**
|
||||
* Singleton provider for OpenTelemetry client instances.
|
||||
* Manages meter and logger providers for telemetry collection.
|
||||
*/
|
||||
export class OpenTelemetryClientProvider {
|
||||
private static _instance: OpenTelemetryClientProvider | null = null
|
||||
|
||||
public static getInstance(): OpenTelemetryClientProvider {
|
||||
if (!OpenTelemetryClientProvider._instance) {
|
||||
OpenTelemetryClientProvider._instance = new OpenTelemetryClientProvider()
|
||||
}
|
||||
return OpenTelemetryClientProvider._instance
|
||||
}
|
||||
|
||||
public static getMeterProvider(): MeterProvider | null {
|
||||
return OpenTelemetryClientProvider.getInstance().meterProvider
|
||||
}
|
||||
|
||||
public static getLoggerProvider(): LoggerProvider | null {
|
||||
return OpenTelemetryClientProvider.getInstance().loggerProvider
|
||||
}
|
||||
|
||||
private readonly meterProvider: MeterProvider | null = null
|
||||
private readonly loggerProvider: LoggerProvider | null = null
|
||||
private readonly config: OpenTelemetryClientValidConfig | null
|
||||
|
||||
/**
|
||||
* Check if debug diagnostics are enabled.
|
||||
* Only log sensitive information (endpoints, headers) when in debug mode.
|
||||
*/
|
||||
private isDebugEnabled(): boolean {
|
||||
return process.env.TEL_DEBUG_DIAGNOSTICS === "true" || process.env.IS_DEV === "true"
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.config = getValidOpenTelemetryConfig()
|
||||
|
||||
if (!this.config) {
|
||||
console.log("[OTEL DEBUG] OpenTelemetry is disabled or not configured")
|
||||
return
|
||||
}
|
||||
|
||||
const isDebugMode = this.isDebugEnabled()
|
||||
|
||||
// Only log endpoint in debug mode (security: avoid exposing infrastructure details)
|
||||
if (isDebugMode) {
|
||||
console.log("[OTEL DEBUG] ========== OpenTelemetry Initialization ==========")
|
||||
console.log(`[OTEL DEBUG] Configuration:`)
|
||||
console.log(`[OTEL DEBUG] - Metrics Exporter: ${this.config.metricsExporter || "none"}`)
|
||||
console.log(`[OTEL DEBUG] - Logs Exporter: ${this.config.logsExporter || "none"}`)
|
||||
console.log(`[OTEL DEBUG] - OTLP Protocol: ${this.config.otlpProtocol || "grpc (default)"}`)
|
||||
|
||||
console.log(`[OTEL DEBUG] - OTLP Endpoint: ${this.config.otlpEndpoint || "not set"}`)
|
||||
console.log(`[OTEL DEBUG] - OTLP Insecure: ${this.config.otlpInsecure || false}`)
|
||||
console.log(`[OTEL DEBUG] - Metric Export Interval: ${this.config.metricExportInterval || 60000}ms`)
|
||||
}
|
||||
|
||||
// Check for headers configuration (via environment variable)
|
||||
const hasHeaders = !!process.env.OTEL_EXPORTER_OTLP_HEADERS
|
||||
if (isDebugMode && hasHeaders) {
|
||||
// In debug mode, show that headers are configured and their total length
|
||||
const headerLength = process.env.OTEL_EXPORTER_OTLP_HEADERS!.length
|
||||
console.log(`[OTEL DEBUG] - OTLP Headers: configured (length: ${headerLength})`)
|
||||
console.log("[OTEL DEBUG] ================================================")
|
||||
}
|
||||
|
||||
// Create resource with service information
|
||||
const resource = new Resource({
|
||||
[ATTR_SERVICE_NAME]: "cline",
|
||||
[ATTR_SERVICE_VERSION]: ExtensionRegistryInfo.version,
|
||||
})
|
||||
|
||||
// Initialize metrics if configured
|
||||
if (this.config.metricsExporter) {
|
||||
this.meterProvider = this.createMeterProvider(resource)
|
||||
}
|
||||
|
||||
// Initialize logs if configured
|
||||
if (this.config.logsExporter) {
|
||||
this.loggerProvider = this.createLoggerProvider(resource)
|
||||
}
|
||||
|
||||
console.log("[OTEL DEBUG] OpenTelemetry initialization complete")
|
||||
}
|
||||
|
||||
private createMeterProvider(resource: Resource): MeterProvider {
|
||||
const exporters = this.config!.metricsExporter!.split(",").map((type) => type.trim())
|
||||
const readers: any[] = []
|
||||
const interval = this.config!.metricExportInterval || 60000
|
||||
const timeout = Math.min(Math.floor(interval * 0.8), 30000)
|
||||
|
||||
console.log(`[OTEL] Creating MeterProvider with exporters: ${exporters.join(", ")}`)
|
||||
|
||||
for (const exporterType of exporters) {
|
||||
try {
|
||||
switch (exporterType) {
|
||||
case "console": {
|
||||
const reader = createConsoleMetricReader(interval, timeout)
|
||||
readers.push(reader)
|
||||
console.log(`[OTEL] Console metrics reader created (interval: ${interval}ms)`)
|
||||
break
|
||||
}
|
||||
case "otlp": {
|
||||
const protocol = this.config!.otlpMetricsProtocol || this.config!.otlpProtocol || "grpc"
|
||||
const endpoint = this.config!.otlpMetricsEndpoint || this.config!.otlpEndpoint
|
||||
const insecure = this.config!.otlpInsecure || false
|
||||
|
||||
if (endpoint) {
|
||||
const reader = createOTLPMetricReader(protocol, endpoint, insecure, interval, timeout)
|
||||
if (reader) {
|
||||
readers.push(reader)
|
||||
console.log(`[OTEL] OTLP metrics reader created (${protocol}, interval: ${interval}ms)`)
|
||||
}
|
||||
} else {
|
||||
console.warn("[OTEL] OTLP metrics exporter requires an endpoint")
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.warn(`[OTEL] Unknown metrics exporter type: ${exporterType}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[OTEL] Failed to create metrics exporter '${exporterType}':`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (readers.length === 0) {
|
||||
console.warn("[OTEL] No metric readers were successfully created")
|
||||
}
|
||||
|
||||
const meterProvider = new MeterProvider({
|
||||
resource,
|
||||
readers,
|
||||
})
|
||||
|
||||
// Set as global meter provider
|
||||
metrics.setGlobalMeterProvider(meterProvider)
|
||||
console.log(`[OTEL] MeterProvider initialized with ${readers.length} reader(s)`)
|
||||
|
||||
return meterProvider
|
||||
}
|
||||
|
||||
private createLoggerProvider(resource: Resource): LoggerProvider {
|
||||
const exporters = this.config!.logsExporter!.split(",").map((type) => type.trim())
|
||||
const loggerProvider = new LoggerProvider({ resource })
|
||||
|
||||
console.log(`[OTEL] Creating LoggerProvider with exporters: ${exporters.join(", ")}`)
|
||||
|
||||
for (const exporterType of exporters) {
|
||||
try {
|
||||
let exporter = null
|
||||
|
||||
switch (exporterType) {
|
||||
case "console":
|
||||
exporter = createConsoleLogExporter()
|
||||
console.log("[OTEL] Console logs exporter created")
|
||||
break
|
||||
case "otlp": {
|
||||
const protocol = this.config!.otlpLogsProtocol || this.config!.otlpProtocol || "grpc"
|
||||
const endpoint = this.config!.otlpLogsEndpoint || this.config!.otlpEndpoint
|
||||
const insecure = this.config!.otlpInsecure || false
|
||||
|
||||
if (endpoint) {
|
||||
exporter = createOTLPLogExporter(protocol, endpoint, insecure)
|
||||
if (exporter) {
|
||||
console.log(`[OTEL] OTLP logs exporter created (${protocol})`)
|
||||
}
|
||||
} else {
|
||||
console.warn("[OTEL] OTLP logs exporter requires an endpoint")
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.warn(`[OTEL] Unknown logs exporter type: ${exporterType}`)
|
||||
}
|
||||
|
||||
if (exporter) {
|
||||
const batchConfig = {
|
||||
maxQueueSize: this.config!.logMaxQueueSize || 2048,
|
||||
maxExportBatchSize: this.config!.logBatchSize || 512,
|
||||
scheduledDelayMillis: this.config!.logBatchTimeout || 5000,
|
||||
}
|
||||
|
||||
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter, batchConfig))
|
||||
|
||||
console.log(
|
||||
`[OTEL] Log batch processor configured: maxQueue=${batchConfig.maxQueueSize}, batchSize=${batchConfig.maxExportBatchSize}, timeout=${batchConfig.scheduledDelayMillis}ms`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[OTEL] Failed to create logs exporter '${exporterType}':`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Set as global logger provider
|
||||
logs.setGlobalLoggerProvider(loggerProvider)
|
||||
console.log("[OTEL] LoggerProvider initialized")
|
||||
|
||||
return loggerProvider
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
if (this.meterProvider) {
|
||||
promises.push(
|
||||
this.meterProvider.shutdown().catch((error) => {
|
||||
console.error("Error shutting down MeterProvider:", error)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (this.loggerProvider) {
|
||||
promises.push(
|
||||
this.loggerProvider.shutdown().catch((error) => {
|
||||
console.error("Error shutting down LoggerProvider:", error)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { credentials as grpcCredentials } from "@grpc/grpc-js"
|
||||
import { OTLPLogExporter as OTLPLogExporterGRPC } from "@opentelemetry/exporter-logs-otlp-grpc"
|
||||
import { OTLPLogExporter as OTLPLogExporterHTTP } from "@opentelemetry/exporter-logs-otlp-http"
|
||||
import { OTLPLogExporter as OTLPLogExporterProto } from "@opentelemetry/exporter-logs-otlp-proto"
|
||||
import { OTLPMetricExporter as OTLPMetricExporterGRPC } from "@opentelemetry/exporter-metrics-otlp-grpc"
|
||||
import { OTLPMetricExporter as OTLPMetricExporterHTTP } from "@opentelemetry/exporter-metrics-otlp-http"
|
||||
import { OTLPMetricExporter as OTLPMetricExporterProto } from "@opentelemetry/exporter-metrics-otlp-proto"
|
||||
import { ConsoleLogRecordExporter, LogRecordExporter } from "@opentelemetry/sdk-logs"
|
||||
import { ConsoleMetricExporter, MetricReader, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"
|
||||
import { wrapLogsExporterWithDiagnostics, wrapMetricsExporterWithDiagnostics } from "./otel-exporter-diagnostics"
|
||||
|
||||
/**
|
||||
* Check if debug diagnostics are enabled
|
||||
*/
|
||||
function isDebugEnabled(): boolean {
|
||||
return process.env.TEL_DEBUG_DIAGNOSTICS === "true" || process.env.IS_DEV === "true"
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a console log exporter
|
||||
*/
|
||||
export function createConsoleLogExporter(): ConsoleLogRecordExporter {
|
||||
return new ConsoleLogRecordExporter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OTLP log exporter based on protocol
|
||||
*/
|
||||
export function createOTLPLogExporter(protocol: string, endpoint: string, insecure: boolean): LogRecordExporter | null {
|
||||
try {
|
||||
let exporter: any = null
|
||||
|
||||
switch (protocol) {
|
||||
case "grpc": {
|
||||
const grpcEndpoint = endpoint.replace(/^https?:\/\//, "")
|
||||
const credentials = insecure ? grpcCredentials.createInsecure() : grpcCredentials.createSsl()
|
||||
|
||||
exporter = new OTLPLogExporterGRPC({
|
||||
url: grpcEndpoint,
|
||||
credentials: credentials,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "http/json": {
|
||||
const logsUrl = endpoint.endsWith("/v1/logs") ? endpoint : `${endpoint}/v1/logs`
|
||||
exporter = new OTLPLogExporterHTTP({ url: logsUrl })
|
||||
break
|
||||
}
|
||||
case "http/protobuf": {
|
||||
const logsUrl = endpoint.endsWith("/v1/logs") ? endpoint : `${endpoint}/v1/logs`
|
||||
exporter = new OTLPLogExporterProto({ url: logsUrl })
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.warn(`[OTEL] Unknown OTLP protocol for logs: ${protocol}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Wrap with diagnostics if debug is enabled
|
||||
if (isDebugEnabled()) {
|
||||
wrapLogsExporterWithDiagnostics(exporter, protocol, endpoint)
|
||||
}
|
||||
|
||||
return exporter
|
||||
} catch (error) {
|
||||
console.error("[OTEL] Error creating OTLP log exporter:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a console metric reader with exporter
|
||||
*/
|
||||
export function createConsoleMetricReader(intervalMs: number, timeoutMs: number): MetricReader {
|
||||
const exporter = new ConsoleMetricExporter()
|
||||
return new PeriodicExportingMetricReader({
|
||||
exporter,
|
||||
exportIntervalMillis: intervalMs,
|
||||
exportTimeoutMillis: timeoutMs,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OTLP metric reader with exporter based on protocol
|
||||
*/
|
||||
export function createOTLPMetricReader(
|
||||
protocol: string,
|
||||
endpoint: string,
|
||||
insecure: boolean,
|
||||
intervalMs: number,
|
||||
timeoutMs: number,
|
||||
): MetricReader | null {
|
||||
try {
|
||||
let exporter: any = null
|
||||
|
||||
switch (protocol) {
|
||||
case "grpc": {
|
||||
const grpcEndpoint = endpoint.replace(/^https?:\/\//, "")
|
||||
const credentials = insecure ? grpcCredentials.createInsecure() : grpcCredentials.createSsl()
|
||||
|
||||
exporter = new OTLPMetricExporterGRPC({
|
||||
url: grpcEndpoint,
|
||||
credentials: credentials,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "http/json": {
|
||||
const metricsUrl = endpoint.endsWith("/v1/metrics") ? endpoint : `${endpoint}/v1/metrics`
|
||||
exporter = new OTLPMetricExporterHTTP({ url: metricsUrl })
|
||||
break
|
||||
}
|
||||
case "http/protobuf": {
|
||||
const metricsUrl = endpoint.endsWith("/v1/metrics") ? endpoint : `${endpoint}/v1/metrics`
|
||||
exporter = new OTLPMetricExporterProto({ url: metricsUrl })
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.warn(`[OTEL] Unknown OTLP protocol for metrics: ${protocol}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Wrap with diagnostics if debug is enabled
|
||||
if (isDebugEnabled()) {
|
||||
wrapMetricsExporterWithDiagnostics(exporter, protocol, endpoint)
|
||||
}
|
||||
|
||||
return new PeriodicExportingMetricReader({
|
||||
exporter,
|
||||
exportIntervalMillis: intervalMs,
|
||||
exportTimeoutMillis: timeoutMs,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[OTEL] Error creating OTLP metric reader:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { Meter } from "@opentelemetry/api"
|
||||
import type { Logger as OTELLogger } from "@opentelemetry/api-logs"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getDistinctId, setDistinctId } from "@/services/logging/distinctId"
|
||||
import { Setting } from "@/shared/proto/index.host"
|
||||
import type { ClineAccountUserInfo } from "../../../auth/AuthService"
|
||||
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "../ITelemetryProvider"
|
||||
import { OpenTelemetryClientProvider } from "./OpenTelemetryClientProvider"
|
||||
|
||||
/**
|
||||
* OpenTelemetry implementation of the telemetry provider interface.
|
||||
* Handles metrics and event logging using OpenTelemetry standards.
|
||||
*/
|
||||
export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
private meter: Meter | null = null
|
||||
private logger: OTELLogger | null = null
|
||||
private telemetrySettings: TelemetrySettings
|
||||
private userAttributes: Record<string, string> = {}
|
||||
// Lazy instrument caches for metrics
|
||||
private counters = new Map<string, ReturnType<Meter["createCounter"]>>()
|
||||
private histograms = new Map<string, ReturnType<Meter["createHistogram"]>>()
|
||||
|
||||
constructor() {
|
||||
// Initialize telemetry settings
|
||||
this.telemetrySettings = {
|
||||
extensionEnabled: true,
|
||||
hostEnabled: true,
|
||||
level: "all",
|
||||
}
|
||||
|
||||
// Get meter and logger from the shared client provider
|
||||
const meterProvider = OpenTelemetryClientProvider.getMeterProvider()
|
||||
const loggerProvider = OpenTelemetryClientProvider.getLoggerProvider()
|
||||
|
||||
if (meterProvider) {
|
||||
this.meter = meterProvider.getMeter("cline")
|
||||
}
|
||||
|
||||
if (loggerProvider) {
|
||||
this.logger = loggerProvider.getLogger("cline")
|
||||
}
|
||||
|
||||
// Log initialization status
|
||||
const loggerReady = !!this.logger
|
||||
const meterReady = !!this.meter
|
||||
if (loggerReady || meterReady) {
|
||||
console.log(`[OTEL] Provider initialized - Logger: ${loggerReady}, Meter: ${meterReady}`)
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize(): Promise<OpenTelemetryTelemetryProvider> {
|
||||
// Listen for host telemetry changes
|
||||
HostProvider.env.subscribeToTelemetrySettings(
|
||||
{},
|
||||
{
|
||||
onResponse: (event) => {
|
||||
const hostEnabled = event.isEnabled === Setting.ENABLED || event.isEnabled === Setting.UNSUPPORTED
|
||||
this.telemetrySettings.hostEnabled = hostEnabled
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Check host-specific telemetry setting (e.g. VS Code setting)
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
this.telemetrySettings.hostEnabled = false
|
||||
}
|
||||
|
||||
this.telemetrySettings.level = await this.getTelemetryLevel()
|
||||
return this
|
||||
}
|
||||
|
||||
public log(event: string, properties?: TelemetryProperties): void {
|
||||
if (!this.isEnabled() || this.telemetrySettings.level === "off") {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter events based on telemetry level
|
||||
if (this.telemetrySettings.level === "error") {
|
||||
if (!event.includes("error")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Record log event (primary path)
|
||||
if (this.logger) {
|
||||
this.logger.emit({
|
||||
severityText: "INFO",
|
||||
body: event,
|
||||
attributes: {
|
||||
distinct_id: getDistinctId(),
|
||||
...this.flattenProperties(properties),
|
||||
...this.userAttributes,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public logRequired(event: string, properties?: TelemetryProperties): void {
|
||||
// Required events always go through regardless of settings
|
||||
if (this.logger) {
|
||||
this.logger.emit({
|
||||
severityText: "INFO",
|
||||
body: event,
|
||||
attributes: {
|
||||
distinct_id: getDistinctId(),
|
||||
_required: true,
|
||||
...this.flattenProperties(properties),
|
||||
...this.userAttributes,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public identifyUser(userInfo: ClineAccountUserInfo, properties: TelemetryProperties = {}): void {
|
||||
const distinctId = getDistinctId()
|
||||
// Only identify user if telemetry is enabled and user ID is different than the currently set distinct ID
|
||||
if (this.isEnabled() && userInfo && userInfo?.id !== distinctId) {
|
||||
// Store user attributes for future events
|
||||
this.userAttributes = {
|
||||
user_id: userInfo.id,
|
||||
user_email: userInfo.email || "",
|
||||
user_name: userInfo.displayName || "",
|
||||
...this.flattenProperties(properties),
|
||||
}
|
||||
|
||||
// Emit identification event
|
||||
if (this.logger) {
|
||||
this.logger.emit({
|
||||
severityText: "INFO",
|
||||
body: "user_identified",
|
||||
attributes: {
|
||||
...this.userAttributes,
|
||||
alias: distinctId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure distinct ID is updated so that we will not identify the user again
|
||||
setDistinctId(userInfo.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Set extension-specific telemetry setting - opt-in/opt-out via UI
|
||||
public setOptIn(optIn: boolean): void {
|
||||
this.telemetrySettings.extensionEnabled = optIn
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this.telemetrySettings.extensionEnabled && this.telemetrySettings.hostEnabled
|
||||
}
|
||||
|
||||
public getSettings(): TelemetrySettings {
|
||||
return { ...this.telemetrySettings }
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a counter metric (lazy creation).
|
||||
* Only creates the counter on first use if meter is available.
|
||||
*/
|
||||
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
|
||||
if (!this.meter) {
|
||||
return
|
||||
}
|
||||
|
||||
let counter = this.counters.get(name)
|
||||
if (!counter) {
|
||||
counter = this.meter.createCounter(name)
|
||||
this.counters.set(name, counter)
|
||||
console.log(`[OTEL] Created counter: ${name}`)
|
||||
}
|
||||
|
||||
counter.add(value, this.flattenProperties(attributes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a histogram metric (lazy creation).
|
||||
* Only creates the histogram on first use if meter is available.
|
||||
*/
|
||||
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
|
||||
if (!this.meter) {
|
||||
return
|
||||
}
|
||||
|
||||
let histogram = this.histograms.get(name)
|
||||
if (!histogram) {
|
||||
histogram = this.meter.createHistogram(name)
|
||||
this.histograms.set(name, histogram)
|
||||
console.log(`[OTEL] Created histogram: ${name}`)
|
||||
}
|
||||
|
||||
histogram.record(value, this.flattenProperties(attributes))
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
// OpenTelemetry client provider handles shutdown
|
||||
// Individual providers don't need to do anything
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current telemetry level from VS Code settings
|
||||
*/
|
||||
private async getTelemetryLevel(): Promise<TelemetrySettings["level"]> {
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
return "off"
|
||||
}
|
||||
const config = vscode.workspace.getConfiguration("telemetry")
|
||||
return config?.get<TelemetrySettings["level"]>("telemetryLevel") || "all"
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten nested properties into dot-notation strings for OpenTelemetry attributes.
|
||||
* OpenTelemetry attributes must be primitives (string, number, boolean).
|
||||
* Adds protection against circular references, prototype pollution, deep graphs,
|
||||
* and limits array sizes to avoid performance issues.
|
||||
*/
|
||||
private flattenProperties(
|
||||
properties?: TelemetryProperties,
|
||||
prefix = "",
|
||||
seen: WeakSet<object> = new WeakSet(),
|
||||
depth = 0,
|
||||
): Record<string, string | number | boolean> {
|
||||
if (!properties) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const flattened: Record<string, string | number | boolean> = {}
|
||||
const MAX_ARRAY_SIZE = 100
|
||||
const MAX_DEPTH = 10
|
||||
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
// Skip prototype pollution vectors
|
||||
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
||||
continue
|
||||
}
|
||||
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
flattened[fullKey] = String(value)
|
||||
} else if (Array.isArray(value)) {
|
||||
// Limit array size to prevent performance issues
|
||||
const limited = value.length > MAX_ARRAY_SIZE ? value.slice(0, MAX_ARRAY_SIZE) : value
|
||||
try {
|
||||
flattened[fullKey] = JSON.stringify(limited)
|
||||
} catch {
|
||||
flattened[fullKey] = "[UnserializableArray]"
|
||||
}
|
||||
if (value.length > MAX_ARRAY_SIZE) {
|
||||
flattened[`${fullKey}_truncated`] = true
|
||||
flattened[`${fullKey}_original_length`] = value.length
|
||||
}
|
||||
} else if (typeof value === "object") {
|
||||
// Handle special objects
|
||||
if (value instanceof Date) {
|
||||
flattened[fullKey] = value.toISOString()
|
||||
continue
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
flattened[fullKey] = value.message
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for circular references
|
||||
if (seen.has(value as object)) {
|
||||
flattened[fullKey] = "[Circular]"
|
||||
continue
|
||||
}
|
||||
// Depth guard
|
||||
if (depth >= MAX_DEPTH) {
|
||||
flattened[fullKey] = "[MaxDepthExceeded]"
|
||||
continue
|
||||
}
|
||||
|
||||
seen.add(value as object)
|
||||
Object.assign(flattened, this.flattenProperties(value as TelemetryProperties, fullKey, seen, depth + 1))
|
||||
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
flattened[fullKey] = value
|
||||
} else {
|
||||
// Fallback: stringify unknown types
|
||||
try {
|
||||
flattened[fullKey] = JSON.stringify(value as unknown as object)
|
||||
} catch {
|
||||
flattened[fullKey] = String(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flattened
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* OpenTelemetry Exporter Diagnostic Utilities
|
||||
*
|
||||
* Provides minimal diagnostic logging for OTLP exporters when debug mode is enabled.
|
||||
* Enable with: TEL_DEBUG_DIAGNOSTICS=true or IS_DEV=true
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wraps a metrics exporter with minimal diagnostic logging
|
||||
*/
|
||||
export function wrapMetricsExporterWithDiagnostics(exporter: any, protocol: string, endpoint: string): void {
|
||||
if (!exporter || typeof exporter.export !== "function") {
|
||||
return
|
||||
}
|
||||
|
||||
const originalExport = exporter.export.bind(exporter)
|
||||
let exportCount = 0
|
||||
|
||||
exporter.export = (metrics: any, resultCallback: any) => {
|
||||
exportCount++
|
||||
const startTime = Date.now()
|
||||
|
||||
const wrappedCallback = (result: any) => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const metricsCount = metrics?.resourceMetrics?.[0]?.scopeMetrics?.[0]?.metrics?.length || 0
|
||||
|
||||
if (result.code === 0) {
|
||||
console.log(
|
||||
`[OTEL METRICS] Export #${exportCount} OK - protocol=${protocol} url=${endpoint} count=${metricsCount} elapsed=${elapsed}ms`,
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
`[OTEL METRICS] Export #${exportCount} FAILED - protocol=${protocol} url=${endpoint} elapsed=${elapsed}ms error="${result.error?.message || "unknown"}"`,
|
||||
)
|
||||
}
|
||||
|
||||
resultCallback(result)
|
||||
}
|
||||
|
||||
try {
|
||||
originalExport(metrics, wrappedCallback)
|
||||
} catch (error) {
|
||||
const elapsed = Date.now() - startTime
|
||||
console.error(
|
||||
`[OTEL METRICS] Export #${exportCount} EXCEPTION - elapsed=${elapsed}ms error="${error instanceof Error ? error.message : String(error)}"`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a logs exporter with minimal diagnostic logging
|
||||
*/
|
||||
export function wrapLogsExporterWithDiagnostics(exporter: any, protocol: string, endpoint: string): void {
|
||||
if (!exporter || typeof exporter.export !== "function") {
|
||||
return
|
||||
}
|
||||
|
||||
const originalExport = exporter.export.bind(exporter)
|
||||
let exportCount = 0
|
||||
|
||||
exporter.export = (logs: any, resultCallback: any) => {
|
||||
exportCount++
|
||||
const startTime = Date.now()
|
||||
|
||||
const wrappedCallback = (result: any) => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const logsCount = logs?.resourceLogs?.[0]?.scopeLogs?.[0]?.logRecords?.length || 0
|
||||
|
||||
if (result.code === 0) {
|
||||
console.log(
|
||||
`[OTEL LOGS] Export #${exportCount} OK - protocol=${protocol} url=${endpoint} count=${logsCount} elapsed=${elapsed}ms`,
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
`[OTEL LOGS] Export #${exportCount} FAILED - protocol=${protocol} url=${endpoint} elapsed=${elapsed}ms error="${result.error?.message || "unknown"}"`,
|
||||
)
|
||||
}
|
||||
|
||||
resultCallback(result)
|
||||
}
|
||||
|
||||
try {
|
||||
originalExport(logs, wrappedCallback)
|
||||
} catch (error) {
|
||||
const elapsed = Date.now() - startTime
|
||||
console.error(
|
||||
`[OTEL LOGS] Export #${exportCount} EXCEPTION - elapsed=${elapsed}ms error="${error instanceof Error ? error.message : String(error)}"`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,17 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
return { ...this.telemetrySettings }
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics are not supported in PostHog provider. These are intentional no-ops.
|
||||
*/
|
||||
public incrementCounter(name: string, value: number = 1, attributes?: TelemetryProperties): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public recordHistogram(name: string, value: number, attributes?: TelemetryProperties): void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
// Only shut down the client if it's not shared (we own it)
|
||||
if (!this.isSharedClient) {
|
||||
|
||||
@@ -71,6 +71,13 @@ export type McpToolCallResponse = {
|
||||
blob?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: "resource_link"
|
||||
uri: string
|
||||
name?: string
|
||||
description?: string
|
||||
mimeType?: string
|
||||
}
|
||||
>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
export interface OpenTelemetryClientConfig {
|
||||
/**
|
||||
* Whether telemetry is enabled via OTEL_TELEMETRY_ENABLED
|
||||
*/
|
||||
enabled: boolean
|
||||
|
||||
/**
|
||||
* Metrics exporter type(s) - can be comma-separated for multiple exporters
|
||||
* Examples: "console", "otlp", "prometheus", "console,otlp"
|
||||
*/
|
||||
metricsExporter?: string
|
||||
|
||||
/**
|
||||
* Logs/events exporter type(s) - can be comma-separated for multiple exporters
|
||||
* Examples: "console", "otlp"
|
||||
*/
|
||||
logsExporter?: string
|
||||
|
||||
/**
|
||||
* Protocol for OTLP exporters: "grpc", "http/json", "http/protobuf"
|
||||
*/
|
||||
otlpProtocol?: string
|
||||
|
||||
/**
|
||||
* General OTLP endpoint (used if specific endpoints not set)
|
||||
*/
|
||||
otlpEndpoint?: string
|
||||
|
||||
/**
|
||||
* Metrics-specific OTLP protocol
|
||||
*/
|
||||
otlpMetricsProtocol?: string
|
||||
|
||||
/**
|
||||
* Metrics-specific OTLP endpoint
|
||||
*/
|
||||
otlpMetricsEndpoint?: string
|
||||
|
||||
/**
|
||||
* Logs-specific OTLP protocol
|
||||
*/
|
||||
otlpLogsProtocol?: string
|
||||
|
||||
/**
|
||||
* Logs-specific OTLP endpoint
|
||||
*/
|
||||
otlpLogsEndpoint?: string
|
||||
|
||||
/**
|
||||
* Metric export interval in milliseconds (for console exporter)
|
||||
*/
|
||||
metricExportInterval?: number
|
||||
|
||||
/**
|
||||
* Whether to use insecure (non-TLS) connections for gRPC OTLP exporters
|
||||
* Set to "true" for local development without TLS
|
||||
* Default: false (uses TLS)
|
||||
*/
|
||||
otlpInsecure?: boolean
|
||||
|
||||
/**
|
||||
* Maximum batch size for log records (default: 512)
|
||||
*/
|
||||
logBatchSize?: number
|
||||
|
||||
/**
|
||||
* Maximum time to wait before exporting logs in milliseconds (default: 5000)
|
||||
*/
|
||||
logBatchTimeout?: number
|
||||
|
||||
/**
|
||||
* Maximum queue size for log records (default: 2048)
|
||||
*/
|
||||
logMaxQueueSize?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type for a valid OpenTelemetry client configuration.
|
||||
* Must have telemetry enabled and at least one exporter configured.
|
||||
*/
|
||||
export interface OpenTelemetryClientValidConfig extends OpenTelemetryClientConfig {
|
||||
enabled: true
|
||||
}
|
||||
|
||||
const isTestEnv = process.env.E2E_TEST === "true" || process.env.IS_TEST === "true"
|
||||
|
||||
/**
|
||||
* Cached OpenTelemetry configuration.
|
||||
* Lazily initialized on first access to avoid race conditions with environment variable loading.
|
||||
*/
|
||||
let otelConfig: OpenTelemetryClientConfig | null = null
|
||||
|
||||
/**
|
||||
* Gets or creates the OpenTelemetry configuration from environment variables.
|
||||
* Configuration is cached after first access for performance.
|
||||
*
|
||||
* Configuration Sources:
|
||||
* - **Production Build**: Environment variables injected by esbuild at build time
|
||||
* via .github/workflows/publish.yml
|
||||
* - **Development**: Environment variables from .env file loaded by VSCode
|
||||
*
|
||||
* Supported Environment Variables:
|
||||
* - OTEL_TELEMETRY_ENABLED: "1" to enable OpenTelemetry (default: off)
|
||||
* - OTEL_METRICS_EXPORTER: Comma-separated list: "console", "otlp", "prometheus"
|
||||
* - OTEL_LOGS_EXPORTER: Comma-separated list: "console", "otlp"
|
||||
* - OTEL_EXPORTER_OTLP_PROTOCOL: "grpc", "http/json", or "http/protobuf"
|
||||
* - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP collector endpoint (if not using specific endpoints)
|
||||
* - OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: Metrics-specific protocol override
|
||||
* - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Metrics-specific endpoint override
|
||||
* - OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: Logs-specific protocol override
|
||||
* - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: Logs-specific endpoint override
|
||||
* - OTEL_METRIC_EXPORT_INTERVAL: Milliseconds between metric exports (default: 60000)
|
||||
* - OTEL_EXPORTER_OTLP_INSECURE: "true" to disable TLS for gRPC (for local development)
|
||||
* - OTEL_LOG_BATCH_SIZE: Maximum batch size for log records (default: 512)
|
||||
* - OTEL_LOG_BATCH_TIMEOUT: Maximum time to wait before exporting logs in ms (default: 5000)
|
||||
* - OTEL_LOG_MAX_QUEUE_SIZE: Maximum queue size for log records (default: 2048)
|
||||
*
|
||||
* @private
|
||||
* @see .env.example for development setup
|
||||
* @see .github/workflows/publish.yml for production environment variable injection
|
||||
*/
|
||||
function getOtelConfig(): OpenTelemetryClientConfig {
|
||||
if (!otelConfig) {
|
||||
otelConfig = {
|
||||
enabled: process.env.OTEL_TELEMETRY_ENABLED === "1",
|
||||
metricsExporter: process.env.OTEL_METRICS_EXPORTER,
|
||||
logsExporter: process.env.OTEL_LOGS_EXPORTER,
|
||||
otlpProtocol: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
|
||||
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
otlpMetricsProtocol: process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
|
||||
otlpMetricsEndpoint: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
|
||||
otlpLogsProtocol: process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
|
||||
otlpLogsEndpoint: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
|
||||
metricExportInterval: process.env.OTEL_METRIC_EXPORT_INTERVAL
|
||||
? parseInt(process.env.OTEL_METRIC_EXPORT_INTERVAL, 10)
|
||||
: undefined,
|
||||
otlpInsecure: process.env.OTEL_EXPORTER_OTLP_INSECURE === "true",
|
||||
logBatchSize: process.env.OTEL_LOG_BATCH_SIZE
|
||||
? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_SIZE, 10))
|
||||
: undefined,
|
||||
logBatchTimeout: process.env.OTEL_LOG_BATCH_TIMEOUT
|
||||
? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_TIMEOUT, 10))
|
||||
: undefined,
|
||||
logMaxQueueSize: process.env.OTEL_LOG_MAX_QUEUE_SIZE
|
||||
? Math.max(1, parseInt(process.env.OTEL_LOG_MAX_QUEUE_SIZE, 10))
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
return otelConfig
|
||||
}
|
||||
|
||||
export function isOpenTelemetryConfigValid(config: OpenTelemetryClientConfig): config is OpenTelemetryClientValidConfig {
|
||||
// Disable in test environment to enable mocking and stubbing
|
||||
if (isTestEnv) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must be explicitly enabled
|
||||
if (!config.enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must have at least one exporter configured
|
||||
return !!(config.metricsExporter || config.logsExporter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets validated OpenTelemetry configuration if available.
|
||||
* Returns null if configuration is invalid or disabled.
|
||||
*
|
||||
* Configuration does not change at runtime - requires VSCode reload to pick up new values.
|
||||
*
|
||||
* @returns Valid OpenTelemetry configuration or null if disabled/invalid
|
||||
* @see .env.example for configuration options
|
||||
*/
|
||||
export function getValidOpenTelemetryConfig(): OpenTelemetryClientValidConfig | null {
|
||||
const config = getOtelConfig()
|
||||
return isOpenTelemetryConfigValid(config) ? config : null
|
||||
}
|
||||
Reference in New Issue
Block a user