2025-06-05 16:04:25 -04:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-12-02 21:27:37 -08:00
import {
DiagLogLevel ,
diag ,
trace ,
context ,
metrics ,
propagation ,
} from '@opentelemetry/api' ;
2025-06-05 16:04:25 -04:00
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc' ;
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc' ;
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc' ;
2025-08-15 18:10:21 -07:00
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http' ;
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http' ;
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http' ;
2025-06-10 21:23:35 +00:00
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' ;
2025-06-05 16:04:25 -04:00
import { NodeSDK } from '@opentelemetry/sdk-node' ;
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' ;
2025-08-19 16:39:59 -07:00
import { resourceFromAttributes } from '@opentelemetry/resources' ;
2025-06-05 16:04:25 -04:00
import {
BatchSpanProcessor ,
ConsoleSpanExporter ,
} from '@opentelemetry/sdk-trace-node' ;
import {
BatchLogRecordProcessor ,
ConsoleLogRecordExporter ,
} from '@opentelemetry/sdk-logs' ;
import {
ConsoleMetricExporter ,
PeriodicExportingMetricReader ,
} from '@opentelemetry/sdk-metrics' ;
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' ;
2025-12-02 21:27:37 -08:00
import type { JWTInput } from 'google-auth-library' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-06-11 04:46:39 +00:00
import { SERVICE_NAME } from './constants.js' ;
2025-06-05 16:04:25 -04:00
import { initializeMetrics } from './metrics.js' ;
2025-06-22 09:26:48 -05:00
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js' ;
2025-07-23 17:48:24 -04:00
import {
FileLogExporter ,
FileMetricExporter ,
FileSpanExporter ,
} from './file-exporters.js' ;
2025-09-17 04:13:57 +09:00
import {
GcpTraceExporter ,
GcpMetricExporter ,
GcpLogExporter ,
} from './gcp-exporters.js' ;
import { TelemetryTarget } from './index.js' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-12-02 21:27:37 -08:00
import { authEvents } from '../code_assist/oauth2.js' ;
2026-02-18 00:11:38 +09:00
import { coreEvents , CoreEvent } from '../utils/events.js' ;
import {
logKeychainAvailability ,
logTokenStorageInitialization ,
} from './loggers.js' ;
import type {
KeychainAvailabilityEvent ,
TokenStorageInitializationEvent ,
} from './types.js' ;
2025-06-05 16:04:25 -04:00
// For troubleshooting, set the log level to DiagLogLevel.DEBUG
2025-12-02 21:27:37 -08:00
class DiagLoggerAdapter {
error ( message : string , . . . args : unknown [ ] ) : void {
debugLogger . error ( message , . . . args ) ;
}
warn ( message : string , . . . args : unknown [ ] ) : void {
debugLogger . warn ( message , . . . args ) ;
}
info ( message : string , . . . args : unknown [ ] ) : void {
debugLogger . log ( message , . . . args ) ;
}
debug ( message : string , . . . args : unknown [ ] ) : void {
debugLogger . debug ( message , . . . args ) ;
}
verbose ( message : string , . . . args : unknown [ ] ) : void {
debugLogger . debug ( message , . . . args ) ;
}
}
diag . setLogger ( new DiagLoggerAdapter ( ) , DiagLogLevel . INFO ) ;
2025-06-05 16:04:25 -04:00
let sdk : NodeSDK | undefined ;
2025-12-03 09:04:13 -08:00
let spanProcessor : BatchSpanProcessor | undefined ;
let logRecordProcessor : BatchLogRecordProcessor | undefined ;
2025-06-05 16:04:25 -04:00
let telemetryInitialized = false ;
2025-12-02 21:27:37 -08:00
let callbackRegistered = false ;
let authListener : ( ( newCredentials : JWTInput ) = > Promise < void > ) | undefined =
undefined ;
2026-02-18 00:11:38 +09:00
let keychainAvailabilityListener :
| ( ( event : KeychainAvailabilityEvent ) = > void )
| undefined = undefined ;
let tokenStorageTypeListener :
| ( ( event : TokenStorageInitializationEvent ) = > void )
| undefined = undefined ;
2025-12-02 21:27:37 -08:00
const telemetryBuffer : Array < ( ) = > void | Promise < void > > = [ ] ;
2025-12-08 11:20:13 -08:00
let activeTelemetryEmail : string | undefined ;
2025-06-05 16:04:25 -04:00
export function isTelemetrySdkInitialized ( ) : boolean {
return telemetryInitialized ;
}
2025-12-02 21:27:37 -08:00
export function bufferTelemetryEvent ( fn : ( ) = > void | Promise < void > ) : void {
if ( telemetryInitialized ) {
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-12-02 21:27:37 -08:00
fn ( ) ;
} else {
telemetryBuffer . push ( fn ) ;
}
}
async function flushTelemetryBuffer ( ) : Promise < void > {
if ( ! telemetryInitialized ) return ;
while ( telemetryBuffer . length > 0 ) {
const fn = telemetryBuffer . shift ( ) ;
if ( fn ) {
try {
await fn ( ) ;
} catch ( e ) {
debugLogger . error ( 'Error executing buffered telemetry event' , e ) ;
}
}
}
}
2025-08-15 18:10:21 -07:00
function parseOtlpEndpoint (
2025-06-05 16:04:25 -04:00
otlpEndpointSetting : string | undefined ,
2025-08-15 18:10:21 -07:00
protocol : 'grpc' | 'http' ,
2025-06-05 16:04:25 -04:00
) : string | undefined {
if ( ! otlpEndpointSetting ) {
return undefined ;
}
// Trim leading/trailing quotes that might come from env variables
const trimmedEndpoint = otlpEndpointSetting . replace ( /^["']|["']$/g , '' ) ;
try {
const url = new URL ( trimmedEndpoint ) ;
2025-08-15 18:10:21 -07:00
if ( protocol === 'grpc' ) {
// OTLP gRPC exporters expect an endpoint in the format scheme://host:port
// The `origin` property provides this, stripping any path, query, or hash.
return url . origin ;
}
// For http, use the full href.
return url . href ;
2025-06-05 16:04:25 -04:00
} catch ( error ) {
diag . error ( 'Invalid OTLP endpoint URL provided:' , trimmedEndpoint , error ) ;
return undefined ;
}
}
2025-12-02 21:27:37 -08:00
export async function initializeTelemetry (
config : Config ,
credentials? : JWTInput ,
) : Promise < void > {
2025-12-08 11:20:13 -08:00
if ( ! config . getTelemetryEnabled ( ) ) {
return ;
}
if ( telemetryInitialized ) {
if (
credentials ? . client_email &&
activeTelemetryEmail &&
credentials . client_email !== activeTelemetryEmail
) {
const message = ` Telemetry credentials have changed (from ${ activeTelemetryEmail } to ${ credentials . client_email } ), but telemetry cannot be re-initialized in this process. Please restart the CLI to use the new account for telemetry. ` ;
debugLogger . error ( message ) ;
}
2025-06-05 16:04:25 -04:00
return ;
}
2025-12-02 21:27:37 -08:00
if ( config . getTelemetryUseCollector ( ) && config . getTelemetryUseCliAuth ( ) ) {
debugLogger . error (
'Telemetry configuration error: "useCollector" and "useCliAuth" cannot both be true. ' +
'CLI authentication is only supported with in-process exporters. ' +
'Disabling telemetry.' ,
) ;
return ;
}
// If using CLI auth and no credentials provided, defer initialization
if ( config . getTelemetryUseCliAuth ( ) && ! credentials ) {
// Register a callback to initialize telemetry when the user logs in.
// This is done only once.
if ( ! callbackRegistered ) {
callbackRegistered = true ;
authListener = async ( newCredentials : JWTInput ) = > {
if ( config . getTelemetryEnabled ( ) && config . getTelemetryUseCliAuth ( ) ) {
2025-12-08 11:20:13 -08:00
debugLogger . log ( 'Telemetry reinit with credentials.' ) ;
2025-12-02 21:27:37 -08:00
await initializeTelemetry ( config , newCredentials ) ;
}
} ;
authEvents . on ( 'post_auth' , authListener ) ;
}
debugLogger . log (
'CLI auth is requested but no credentials, deferring telemetry initialization.' ,
) ;
return ;
}
2025-08-19 16:39:59 -07:00
const resource = resourceFromAttributes ( {
2025-06-05 16:04:25 -04:00
[ SemanticResourceAttributes . SERVICE_NAME ] : SERVICE_NAME ,
2025-06-09 09:31:27 -07:00
[ SemanticResourceAttributes . SERVICE_VERSION ] : process . version ,
2025-06-11 04:46:39 +00:00
'session.id' : config . getSessionId ( ) ,
2025-06-05 16:04:25 -04:00
} ) ;
2026-02-18 00:11:38 +09:00
if ( ! keychainAvailabilityListener ) {
keychainAvailabilityListener = ( event : KeychainAvailabilityEvent ) = > {
logKeychainAvailability ( config , event ) ;
} ;
coreEvents . on (
CoreEvent . TelemetryKeychainAvailability ,
keychainAvailabilityListener ,
) ;
}
if ( ! tokenStorageTypeListener ) {
tokenStorageTypeListener = ( event : TokenStorageInitializationEvent ) = > {
logTokenStorageInitialization ( config , event ) ;
} ;
coreEvents . on (
CoreEvent . TelemetryTokenStorageType ,
tokenStorageTypeListener ,
) ;
}
2025-06-13 10:27:22 -07:00
const otlpEndpoint = config . getTelemetryOtlpEndpoint ( ) ;
2025-08-15 18:10:21 -07:00
const otlpProtocol = config . getTelemetryOtlpProtocol ( ) ;
2025-09-17 04:13:57 +09:00
const telemetryTarget = config . getTelemetryTarget ( ) ;
const useCollector = config . getTelemetryUseCollector ( ) ;
2025-12-02 21:27:37 -08:00
2025-08-15 18:10:21 -07:00
const parsedEndpoint = parseOtlpEndpoint ( otlpEndpoint , otlpProtocol ) ;
2025-07-23 17:48:24 -04:00
const telemetryOutfile = config . getTelemetryOutfile ( ) ;
2025-09-23 01:40:30 +09:00
const useOtlp = ! ! parsedEndpoint && ! telemetryOutfile ;
2025-06-05 16:04:25 -04:00
2025-09-17 04:13:57 +09:00
const gcpProjectId =
process . env [ 'OTLP_GOOGLE_CLOUD_PROJECT' ] ||
process . env [ 'GOOGLE_CLOUD_PROJECT' ] ;
const useDirectGcpExport =
2025-12-02 21:27:37 -08:00
telemetryTarget === TelemetryTarget . GCP && ! useCollector ;
2025-09-17 04:13:57 +09:00
2025-08-15 18:10:21 -07:00
let spanExporter :
| OTLPTraceExporter
| OTLPTraceExporterHttp
2025-09-17 04:13:57 +09:00
| GcpTraceExporter
2025-08-15 18:10:21 -07:00
| FileSpanExporter
| ConsoleSpanExporter ;
let logExporter :
| OTLPLogExporter
| OTLPLogExporterHttp
2025-09-17 04:13:57 +09:00
| GcpLogExporter
2025-08-15 18:10:21 -07:00
| FileLogExporter
| ConsoleLogRecordExporter ;
let metricReader : PeriodicExportingMetricReader ;
2025-09-17 04:13:57 +09:00
if ( useDirectGcpExport ) {
2025-12-02 21:27:37 -08:00
debugLogger . log (
'Creating GCP exporters with projectId:' ,
gcpProjectId ,
'using' ,
credentials ? 'provided credentials' : 'ADC' ,
) ;
spanExporter = new GcpTraceExporter ( gcpProjectId , credentials ) ;
logExporter = new GcpLogExporter ( gcpProjectId , credentials ) ;
2025-09-17 04:13:57 +09:00
metricReader = new PeriodicExportingMetricReader ( {
2025-12-02 21:27:37 -08:00
exporter : new GcpMetricExporter ( gcpProjectId , credentials ) ,
2025-09-17 04:13:57 +09:00
exportIntervalMillis : 30000 ,
} ) ;
} else if ( useOtlp ) {
2025-08-15 18:10:21 -07:00
if ( otlpProtocol === 'http' ) {
2026-03-06 20:58:00 +01:00
const buildUrl = ( path : string ) = > {
const url = new URL ( parsedEndpoint ) ;
// Join the existing pathname with the new path, handling trailing slashes.
url . pathname = [ url . pathname . replace ( /\/$/ , '' ) , path ] . join ( '/' ) ;
return url . href ;
} ;
2025-08-15 18:10:21 -07:00
spanExporter = new OTLPTraceExporterHttp ( {
2026-03-06 20:58:00 +01:00
url : buildUrl ( 'v1/traces' ) ,
2025-08-15 18:10:21 -07:00
} ) ;
logExporter = new OTLPLogExporterHttp ( {
2026-03-06 20:58:00 +01:00
url : buildUrl ( 'v1/logs' ) ,
2025-08-15 18:10:21 -07:00
} ) ;
metricReader = new PeriodicExportingMetricReader ( {
exporter : new OTLPMetricExporterHttp ( {
2026-03-06 20:58:00 +01:00
url : buildUrl ( 'v1/metrics' ) ,
2025-08-15 18:10:21 -07:00
} ) ,
exportIntervalMillis : 10000 ,
} ) ;
} else {
// grpc
spanExporter = new OTLPTraceExporter ( {
url : parsedEndpoint ,
2025-06-10 21:23:35 +00:00
compression : CompressionAlgorithm.GZIP ,
2025-08-15 18:10:21 -07:00
} ) ;
logExporter = new OTLPLogExporter ( {
url : parsedEndpoint ,
2025-06-10 21:23:35 +00:00
compression : CompressionAlgorithm.GZIP ,
2025-08-15 18:10:21 -07:00
} ) ;
metricReader = new PeriodicExportingMetricReader ( {
2025-06-10 21:23:35 +00:00
exporter : new OTLPMetricExporter ( {
2025-08-15 18:10:21 -07:00
url : parsedEndpoint ,
2025-06-10 21:23:35 +00:00
compression : CompressionAlgorithm.GZIP ,
} ) ,
2025-06-05 16:04:25 -04:00
exportIntervalMillis : 10000 ,
2025-08-15 18:10:21 -07:00
} ) ;
}
} else if ( telemetryOutfile ) {
spanExporter = new FileSpanExporter ( telemetryOutfile ) ;
logExporter = new FileLogExporter ( telemetryOutfile ) ;
metricReader = new PeriodicExportingMetricReader ( {
exporter : new FileMetricExporter ( telemetryOutfile ) ,
exportIntervalMillis : 10000 ,
} ) ;
} else {
spanExporter = new ConsoleSpanExporter ( ) ;
logExporter = new ConsoleLogRecordExporter ( ) ;
metricReader = new PeriodicExportingMetricReader ( {
exporter : new ConsoleMetricExporter ( ) ,
exportIntervalMillis : 10000 ,
} ) ;
}
2025-06-05 16:04:25 -04:00
2025-12-03 09:04:13 -08:00
// Store processor references for manual flushing
spanProcessor = new BatchSpanProcessor ( spanExporter ) ;
logRecordProcessor = new BatchLogRecordProcessor ( logExporter ) ;
2025-06-05 16:04:25 -04:00
sdk = new NodeSDK ( {
resource ,
2025-12-03 09:04:13 -08:00
spanProcessors : [ spanProcessor ] ,
logRecordProcessors : [ logRecordProcessor ] ,
2025-06-05 16:04:25 -04:00
metricReader ,
instrumentations : [ new HttpInstrumentation ( ) ] ,
} ) ;
try {
2025-12-16 21:28:18 -08:00
sdk . start ( ) ;
2025-08-13 10:38:45 +09:00
if ( config . getDebugMode ( ) ) {
2025-10-21 16:35:22 -04:00
debugLogger . log ( 'OpenTelemetry SDK started successfully.' ) ;
2025-08-13 10:38:45 +09:00
}
2025-06-05 16:04:25 -04:00
telemetryInitialized = true ;
2025-12-08 11:20:13 -08:00
activeTelemetryEmail = credentials ? . client_email ;
2025-06-11 16:50:24 +00:00
initializeMetrics ( config ) ;
2025-12-02 21:27:37 -08:00
void flushTelemetryBuffer ( ) ;
2025-06-05 16:04:25 -04:00
} catch ( error ) {
2025-12-29 15:46:10 -05:00
debugLogger . error ( 'Error starting OpenTelemetry SDK:' , error ) ;
2025-06-05 16:04:25 -04:00
}
2025-12-03 09:04:13 -08:00
// Note: We don't use process.on('exit') here because that callback is synchronous
// and won't wait for the async shutdownTelemetry() to complete.
// Instead, telemetry shutdown is handled in runExitCleanup() in cleanup.ts
2025-08-13 10:38:45 +09:00
process . on ( 'SIGTERM' , ( ) = > {
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-08-13 10:38:45 +09:00
shutdownTelemetry ( config ) ;
} ) ;
process . on ( 'SIGINT' , ( ) = > {
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
2025-08-13 10:38:45 +09:00
shutdownTelemetry ( config ) ;
} ) ;
2025-12-03 09:04:13 -08:00
}
/ * *
* Force flush all pending telemetry data to disk .
* This is useful for ensuring telemetry is written before critical operations like / clear .
* /
export async function flushTelemetry ( config : Config ) : Promise < void > {
if ( ! telemetryInitialized || ! spanProcessor || ! logRecordProcessor ) {
return ;
}
try {
// Force flush all pending telemetry to disk
await Promise . all ( [
spanProcessor . forceFlush ( ) ,
logRecordProcessor . forceFlush ( ) ,
] ) ;
if ( config . getDebugMode ( ) ) {
debugLogger . log ( 'OpenTelemetry SDK flushed successfully.' ) ;
}
} catch ( error ) {
2025-12-29 15:46:10 -05:00
debugLogger . error ( 'Error flushing SDK:' , error ) ;
2025-12-03 09:04:13 -08:00
}
2025-06-05 16:04:25 -04:00
}
2025-12-02 21:27:37 -08:00
export async function shutdownTelemetry (
config : Config ,
fromProcessExit = true ,
) : Promise < void > {
2025-06-05 16:04:25 -04:00
if ( ! telemetryInitialized || ! sdk ) {
return ;
}
try {
2025-12-16 21:28:18 -08:00
ClearcutLogger . getInstance ( ) ? . shutdown ( ) ;
2025-06-05 16:04:25 -04:00
await sdk . shutdown ( ) ;
2025-12-02 21:27:37 -08:00
if ( config . getDebugMode ( ) && fromProcessExit ) {
2025-10-21 16:35:22 -04:00
debugLogger . log ( 'OpenTelemetry SDK shut down successfully.' ) ;
2025-08-13 10:38:45 +09:00
}
2025-06-05 16:04:25 -04:00
} catch ( error ) {
2025-12-29 15:46:10 -05:00
debugLogger . error ( 'Error shutting down SDK:' , error ) ;
2025-06-05 16:04:25 -04:00
} finally {
telemetryInitialized = false ;
2025-12-02 21:27:37 -08:00
sdk = undefined ;
// Fully reset the global APIs to allow for re-initialization.
// This is primarily for testing environments where the SDK is started
// and stopped multiple times in the same process.
trace . disable ( ) ;
context . disable ( ) ;
metrics . disable ( ) ;
propagation . disable ( ) ;
diag . disable ( ) ;
if ( authListener ) {
authEvents . off ( 'post_auth' , authListener ) ;
authListener = undefined ;
}
2026-02-18 00:11:38 +09:00
if ( keychainAvailabilityListener ) {
coreEvents . off (
CoreEvent . TelemetryKeychainAvailability ,
keychainAvailabilityListener ,
) ;
keychainAvailabilityListener = undefined ;
}
if ( tokenStorageTypeListener ) {
coreEvents . off (
CoreEvent . TelemetryTokenStorageType ,
tokenStorageTypeListener ,
) ;
tokenStorageTypeListener = undefined ;
}
2025-12-02 21:27:37 -08:00
callbackRegistered = false ;
2025-12-08 11:20:13 -08:00
activeTelemetryEmail = undefined ;
2025-06-05 16:04:25 -04:00
}
}