import * as http from 'http'; import * as https from 'https'; import honeycombBeeline from 'honeycomb-beeline'; import { config } from '../config'; import { NestedObject, Nullable } from '../utils/types'; /* eslint @typescript-eslint/no-unsafe-call: 0, @typescript-eslint/no-unsafe-assignment: 0 */ /* eslint @typescript-eslint/no-unsafe-member-access: 0, @typescript-eslint/no-unsafe-return: 0 */ class Honeycomb { public static honey: any; public static initialized = false; // ToDo Can be shortened. Pass config as argument. public static initialize(): void { try { if (config.honeyComb.writeKey && config.honeyComb.dataset) { Honeycomb.honey = honeycombBeeline({ writeKey: config.honeyComb.writeKey, dataset: config.honeyComb.dataset, disableInstrumentation: true, batchTimeTrigger: 10 * 1000, batchSizeTrigger: 200, }); Honeycomb.initialized = true; } else { Honeycomb.initialized = false; } } catch { Honeycomb.initialized = false; } } public static start(fields: object, traceId?: string, parentSpanId?: string): object { const honeyComb = Honeycomb.initialized ? Honeycomb.honey.startTrace(fields, traceId, parentSpanId) : {}; Honeycomb.addCustomContext( { mediaviewVersion: config.version, namespace: config.namespace, }, ); return honeyComb; } public static startWithHeaders(fields: object, header?: string): Record { if (header) { const { traceId, parentSpanId } = Honeycomb.honey.unmarshalTraceContext(header); if (Honeycomb.initialized) { const honeyComb = Honeycomb.honey.startTrace(fields); Honeycomb.addCustomContext( { mediaviewVersion: config.version, namespace: config.namespace, fe_trace_id: traceId, fe_parent_span: parentSpanId, }, ); return honeyComb; } } // init without tracing header, otherwise testing will fail if (Honeycomb.initialized) { const honeyComb = Honeycomb.honey.startTrace(fields); Honeycomb.addCustomContext( { mediaviewVersion: config.version, namespace: config.namespace, }, ); return honeyComb; } return {}; } public static sortKeys(obj: Array> | NestedObject, excludeKeys: NestedObject = {}): Array | object { if (Array.isArray(obj)) { const arr2: any[] = []; for (const item of obj) { arr2.push(Honeycomb.sortKeys(item, excludeKeys)); } return arr2.sort(); } if (typeof obj === 'object' && !!obj) { const keys: string[] = Object.keys(obj).sort(); const obj2: any = {}; for (const key of keys) { // Excluding keys if (excludeKeys[key] !== true) { // N.B. recursive call const excludeNext = typeof excludeKeys[key] === 'object' ? excludeKeys[key] as NestedObject : {}; obj2[key] = Honeycomb.sortKeys(obj[key], excludeNext); } } return obj2; } return obj; } public static traceActive(): boolean { return !!Honeycomb.honey?.traceActive(); } public static addCustomContext(customContextObject: Record): void { for (const [key, value] of Object.entries(customContextObject)) { Honeycomb.honey?.customContext.add(key, value); } } public static startSpan(jsmodule: string, task: string, customContext?: Record): Record { if (Honeycomb.traceActive()) { const span = Honeycomb.honey.startSpan({ jsmodule, name: task, service: 'mediaView' }); if (customContext) { Honeycomb.addCustomContext(customContext); } return span; } return {}; } public static startAsyncSpan( jsmodule: string, task: string, asyncFunc: (span: Record) => T, customContext?: Record, ): T { if (Honeycomb.traceActive()) { const result = Honeycomb.honey.startAsyncSpan({ jsmodule, name: task, service: 'mediaView', ...customContext, }, asyncFunc); if (customContext) { Honeycomb.addCustomContext(customContext); } return result; } return asyncFunc({}); } public static endSpan(span: object): void { if (Honeycomb.traceActive() && span) { Honeycomb.honey.finishSpan(span); } } public static finishTrace(trace?: object): void { if (Honeycomb.traceActive() && trace) { Honeycomb.honey.finishTrace(trace); } } } /** * Adds honeycomb tracing beacons to the specified socket. * @param socket - The socket to trace. * @param secure - Indicates whether this is a SSL socket. */ function traceSocket(socket: NodeJS.Socket, secure: boolean) { const h = Honeycomb.honey; Honeycomb.startAsyncSpan('socket', 'socket', (asyncSpan) => { const lookupSpan = Honeycomb.startSpan('socket', 'lookup'); let connectSpan: object; let secureConnectSpan: object; let bytesTransferred = 0; socket.once('lookup', h.bindFunctionToTrace( (_error: Nullable, ip: string, _ipVersion: Nullable, host: string) => { h.addContext({ host, ip }); Honeycomb.endSpan(lookupSpan); connectSpan = Honeycomb.startSpan('socket', 'connect'); }, )); socket.once('connect', h.bindFunctionToTrace(() => { Honeycomb.endSpan(connectSpan); if (secure) { secureConnectSpan = Honeycomb.startSpan('socket', 'secureConnect'); } })); if (secure) { socket.once('secureConnect', h.bindFunctionToTrace(() => { Honeycomb.endSpan(secureConnectSpan); })); } socket.on('data', (chunk) => { bytesTransferred += chunk.length; }); socket.once('close', h.bindFunctionToTrace(() => { h.addContext({ bytesTransferred }); Honeycomb.endSpan(asyncSpan); })); }); } class TracedHttpAgent extends http.Agent { public createConnection: () => NodeJS.Socket; public constructor(options?: http.AgentOptions) { super(options); // for some reason the http.Agent class is not fully defined in @types/node // @ts-ignore const originalCreateConnection = this.createConnection; this.createConnection = (...args) => { const socket = Reflect.apply(originalCreateConnection, this, args); traceSocket(socket, false); return socket; }; } } class TracedHttpsAgent extends https.Agent { public createConnection: () => NodeJS.Socket; public constructor(options?: https.AgentOptions) { super(options); // for some reason the http.Agent class is not fully defined in @types/node // @ts-ignore const originalCreateConnection = this.createConnection; this.createConnection = (...args) => { const socket = Reflect.apply(originalCreateConnection, this, args); traceSocket(socket, true); return socket; }; } } export { Honeycomb, TracedHttpAgent, TracedHttpsAgent };