packages/synthetics-sdk-api/src/auto_instrumentation.ts (114 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { BatchSpanProcessor, AlwaysOnSampler, } from '@opentelemetry/sdk-trace-base'; import { Span, TraceFlags } from '@opentelemetry/api'; import { GoogleAuth, GoogleAuthOptions } from 'google-auth-library'; import { Logger } from 'winston'; import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter'; const LOGGING_TRACE_KEY = 'logging.googleapis.com/trace'; const LOGGING_SPAN_KEY = 'logging.googleapis.com/spanId'; const LOGGING_SAMPLED_KEY = 'logging.googleapis.com/trace_sampled'; const LOGGING_SEVERITY_KEY = 'severity'; const levelToSeverityMap: { [key: string]: string } = { error: 'ERROR', warn: 'WARNING', info: 'INFO', http: 'INFO', verbose: 'DEBUG', debug: 'DEBUG', silly: 'DEBUG', }; let singletonAutoInstrumentation: SyntheticsAutoInstrumentation | null; /** * @public * * This function sets up user authored synthetic code with a baseline open * telemetry setup that will write traces and logs to cloud trace and cloud * logging. * * NOTE: For this module to be used effectively, it needs to be included * and ran before any other code within your synthetic application runs. */ export const instantiateAutoInstrumentation = ( args: { googleAuthOptions?: GoogleAuthOptions } = {} ) => { singletonAutoInstrumentation = new SyntheticsAutoInstrumentation(args); }; /** * @public * * Returns a winston logger that is instrumented to use the console transport. * * If {@link #instantiateAutoInstrumentation} is ran prior to any other code, * and a project id is detected according to the logs will be instrumented * with trace information that is formated in gcp's * {@link https://cloud.google.com/logging/docs/structured-logging|structured logging} * format. */ export const getInstrumentedLogger = async (): Promise<Logger> => { if (singletonAutoInstrumentation) { return await singletonAutoInstrumentation.getInstrumentedLogger(); } else { const winston = require('winston'); return winston.createLogger({ transports: [new winston.transports.Console()], }); } }; class SyntheticsAutoInstrumentation { provider: NodeTracerProvider; private logger: Logger; private gcpProjectId?: string | null; private authArgs: GoogleAuthOptions; constructor(args: { googleAuthOptions?: GoogleAuthOptions } = {}) { this.authArgs = args.googleAuthOptions || {}; this.provider = new NodeTracerProvider({ sampler: new AlwaysOnSampler(), }); const exporter = new TraceExporter(); this.provider.addSpanProcessor(new BatchSpanProcessor(exporter)); this.provider.register(); // add node auto instrumentation registerInstrumentations({ instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-winston': { logHook: (span: Span, record: Record<string, string | boolean>) => { // If the auto instrumentation has detected a project id, convert // otel fields that are automatically added to the record to use // structured logging fields instead. if (this.gcpProjectId) { record[LOGGING_TRACE_KEY] = `projects/${ this.gcpProjectId }/traces/${span.spanContext().traceId}`; record[LOGGING_SPAN_KEY] = span.spanContext().spanId; record[LOGGING_SAMPLED_KEY] = span.spanContext().traceFlags === TraceFlags.SAMPLED; record[LOGGING_SEVERITY_KEY] = levelToSeverityMap[String(record.level)] ?? 'DEFAULT'; delete record['span_id']; delete record['trace_flags']; delete record['level']; delete record['trace_id']; } }, }, }), ], }); // Require dependencies after instrumentation is registered, // otherwise they wont be instrumented. const winston = require('winston'); const logger = winston.createLogger({ transports: [new winston.transports.Console()], }); this.logger = logger; } async getInstrumentedLogger(): Promise<Logger> { this.gcpProjectId = await resolveProjectId( this.gcpProjectId, this.authArgs ); return this.logger; } } /** * @public * * Resolves and caches the project ID, a field that is required for formatting * structured logs. */ export const resolveProjectId = async ( gcpProjectId?: string | null, googleAuthOptions?: GoogleAuthOptions ): Promise<string | null> => { // if gcpProjectId has been instantiated, return it. Otherwise attempt to // resolve at most 1 times, assign to null otherwise. if (typeof gcpProjectId === 'string' || gcpProjectId === null) { return gcpProjectId; } const auth = new GoogleAuth({ credentials: googleAuthOptions?.credentials, keyFile: googleAuthOptions?.keyFile, keyFilename: googleAuthOptions?.keyFilename, projectId: googleAuthOptions?.projectId, scopes: ['https://www.googleapis.com/auth/cloud-platform'], }); try { return await auth.getProjectId(); } catch (e) { console.log( 'Unable to resolve gcpProjectId, logs will not be written in GCP Structured Logging format' ); } return null; };