mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
429 lines
12 KiB
TypeScript
429 lines
12 KiB
TypeScript
import OpenTelemetryAPI, {
|
|
/*
|
|
* diag,
|
|
* DiagConsoleLogger,
|
|
* DiagLogLevel,
|
|
*/
|
|
Meter,
|
|
type AttributeValue,
|
|
} from "@opentelemetry/api";
|
|
import { Logger, logs } from "@opentelemetry/api-logs";
|
|
import {
|
|
Counter,
|
|
Histogram,
|
|
MetricOptions,
|
|
} from "@opentelemetry/api/build/src/metrics/Metric";
|
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
import { AWSXRayIdGenerator } from "@opentelemetry/id-generator-aws-xray";
|
|
import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
|
|
import { Resource } from "@opentelemetry/resources";
|
|
import {
|
|
BatchLogRecordProcessor,
|
|
LoggerProvider,
|
|
LogRecordProcessor,
|
|
type LoggerProviderConfig,
|
|
} from "@opentelemetry/sdk-logs";
|
|
import type { Resource as LogsResource } from "@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources/build/src/Resource";
|
|
import {
|
|
Aggregation,
|
|
MeterProvider,
|
|
PeriodicExportingMetricReader,
|
|
} from "@opentelemetry/sdk-metrics";
|
|
import type { PushMetricExporter } from "@opentelemetry/sdk-metrics/build/src/export/MetricExporter";
|
|
import * as opentelemetry from "@opentelemetry/sdk-node";
|
|
import { SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
import {
|
|
ATTR_SERVICE_NAME,
|
|
ATTR_SERVICE_VERSION,
|
|
} from "@opentelemetry/semantic-conventions";
|
|
import URL from "../../Types/API/URL";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
import { AppVersion, Env, DisableTelemetry } from "../EnvironmentConfig";
|
|
import logger from "./Logger";
|
|
|
|
type ResourceWithRawAttributes = LogsResource & {
|
|
getRawAttributes?: () => Array<[string, AttributeValue | undefined]>;
|
|
};
|
|
|
|
/*
|
|
* Enable this line to see debug logs
|
|
* diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
|
*/
|
|
|
|
export type Span = opentelemetry.api.Span;
|
|
export type SpanStatus = opentelemetry.api.SpanStatus;
|
|
export type SpanException = opentelemetry.api.Exception;
|
|
export type SpanOptions = opentelemetry.api.SpanOptions;
|
|
export type TelemetryLogger = Logger;
|
|
export type TelemetryAttributes = opentelemetry.api.Attributes;
|
|
export type TelemetryCounter = Counter<opentelemetry.api.Attributes>;
|
|
|
|
export enum SpanStatusCode {
|
|
UNSET = 0,
|
|
OK = 1,
|
|
ERROR = 2,
|
|
}
|
|
|
|
export default class Telemetry {
|
|
public static sdk: opentelemetry.NodeSDK | null = null;
|
|
|
|
public static logger: Logger | null = null;
|
|
|
|
public static meter: Meter | null = null;
|
|
|
|
public static meterProvider: MeterProvider | null = null;
|
|
|
|
public static loggerProvider: LoggerProvider | null = null;
|
|
|
|
public static metricReader: PeriodicExportingMetricReader | undefined;
|
|
|
|
public static serviceName: string | null = null;
|
|
|
|
public static getHeaders(): Dictionary<string> {
|
|
if (!process.env["OPENTELEMETRY_EXPORTER_OTLP_HEADERS"]) {
|
|
return {};
|
|
}
|
|
|
|
const headersStrings: Array<string> =
|
|
process.env["OPENTELEMETRY_EXPORTER_OTLP_HEADERS"].split(";");
|
|
|
|
const headers: Dictionary<string> = {};
|
|
|
|
for (const headerString of headersStrings) {
|
|
const header: Array<string> = headerString.split("=");
|
|
if (header.length === 2) {
|
|
headers[header[0]!.toString()] = header[1]!.toString();
|
|
}
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
public static getOtlpEndpoint(): URL | null {
|
|
if (!process.env["OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT"]) {
|
|
return null;
|
|
}
|
|
|
|
return URL.fromString(
|
|
process.env["OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT"] || "",
|
|
);
|
|
}
|
|
|
|
public static getOltpLogsEndpoint(): URL | null {
|
|
const oltpEndpoint: URL | null = this.getOtlpEndpoint();
|
|
|
|
if (!oltpEndpoint) {
|
|
return null;
|
|
}
|
|
|
|
return URL.fromString(oltpEndpoint.toString() + "/v1/logs");
|
|
}
|
|
|
|
public static getOltpMetricsEndpoint(): URL | null {
|
|
const oltpEndpoint: URL | null = this.getOtlpEndpoint();
|
|
|
|
if (!oltpEndpoint) {
|
|
return null;
|
|
}
|
|
|
|
return URL.fromString(oltpEndpoint.toString() + "/v1/metrics");
|
|
}
|
|
|
|
public static getOltpTracesEndpoint(): URL | null {
|
|
const oltpEndpoint: URL | null = this.getOtlpEndpoint();
|
|
|
|
if (!oltpEndpoint) {
|
|
return null;
|
|
}
|
|
|
|
return URL.fromString(oltpEndpoint.toString() + "/v1/traces");
|
|
}
|
|
|
|
public static getResource(data: { serviceName: string }): Resource {
|
|
return new Resource({
|
|
[ATTR_SERVICE_NAME]: data.serviceName,
|
|
[ATTR_SERVICE_VERSION]: AppVersion,
|
|
["deployment.environment"]: Env,
|
|
});
|
|
}
|
|
|
|
public static init(data: {
|
|
serviceName: string;
|
|
}): opentelemetry.NodeSDK | null {
|
|
this.serviceName = data.serviceName;
|
|
|
|
if (DisableTelemetry) {
|
|
return null;
|
|
}
|
|
|
|
if (!this.sdk) {
|
|
const headers: Dictionary<string> = this.getHeaders();
|
|
|
|
const hasHeaders: boolean = Object.keys(headers).length > 0;
|
|
|
|
let traceExporter: SpanExporter | undefined = undefined;
|
|
|
|
if (this.getOltpTracesEndpoint() && hasHeaders) {
|
|
traceExporter = new OTLPTraceExporter({
|
|
url: this.getOltpTracesEndpoint()!.toString(),
|
|
headers: headers,
|
|
compression: CompressionAlgorithm.GZIP,
|
|
}) as unknown as SpanExporter;
|
|
}
|
|
|
|
if (this.getOltpMetricsEndpoint() && hasHeaders) {
|
|
const metricExporter: PushMetricExporter = new OTLPMetricExporter({
|
|
url: this.getOltpMetricsEndpoint()!.toString(),
|
|
headers: headers,
|
|
compression: CompressionAlgorithm.GZIP,
|
|
}) as unknown as PushMetricExporter;
|
|
|
|
// Force an SDK-side aggregation selector that matches the modern metrics API.
|
|
if (
|
|
typeof (metricExporter as { selectAggregation?: unknown })
|
|
.selectAggregation === "function"
|
|
) {
|
|
(
|
|
metricExporter as unknown as {
|
|
selectAggregation: (..._args: Array<unknown>) => Aggregation;
|
|
}
|
|
).selectAggregation = () => {
|
|
return Aggregation.Default();
|
|
};
|
|
}
|
|
|
|
this.metricReader = new PeriodicExportingMetricReader({
|
|
exporter: metricExporter,
|
|
});
|
|
}
|
|
|
|
const resource: Resource = this.getResource({
|
|
serviceName: data.serviceName,
|
|
});
|
|
|
|
const logRecordProcessors: Array<LogRecordProcessor> = [];
|
|
|
|
const loggerProviderResource: ResourceWithRawAttributes =
|
|
resource as unknown as ResourceWithRawAttributes;
|
|
|
|
if (typeof loggerProviderResource.getRawAttributes !== "function") {
|
|
loggerProviderResource.getRawAttributes = () => {
|
|
return Object.entries(resource.attributes) as Array<
|
|
[string, AttributeValue | undefined]
|
|
>;
|
|
};
|
|
}
|
|
|
|
if (this.getOltpLogsEndpoint() && hasHeaders) {
|
|
const logExporter: OTLPLogExporter = new OTLPLogExporter({
|
|
url: this.getOltpLogsEndpoint()!.toString(),
|
|
headers: headers,
|
|
compression: CompressionAlgorithm.GZIP,
|
|
});
|
|
|
|
logRecordProcessors.push(new BatchLogRecordProcessor(logExporter));
|
|
}
|
|
|
|
const loggerProviderConfig: LoggerProviderConfig = {
|
|
resource: loggerProviderResource,
|
|
};
|
|
|
|
if (logRecordProcessors.length > 0) {
|
|
(
|
|
loggerProviderConfig as LoggerProviderConfig & {
|
|
processors?: Array<LogRecordProcessor>;
|
|
}
|
|
).processors = logRecordProcessors;
|
|
}
|
|
|
|
this.loggerProvider = new LoggerProvider(loggerProviderConfig);
|
|
|
|
logs.setGlobalLoggerProvider(this.loggerProvider);
|
|
|
|
const nodeSdkConfiguration: Partial<opentelemetry.NodeSDKConfiguration> =
|
|
{
|
|
idGenerator: new AWSXRayIdGenerator(),
|
|
instrumentations: [],
|
|
resource:
|
|
loggerProviderResource as unknown as opentelemetry.NodeSDKConfiguration["resource"],
|
|
autoDetectResources: true,
|
|
};
|
|
|
|
if (traceExporter) {
|
|
nodeSdkConfiguration.traceExporter = traceExporter;
|
|
}
|
|
|
|
/*
|
|
* We will skip this becasue we're attachng this metric reader to the meter provider later.
|
|
* if (this.metricReader) {
|
|
* nodeSdkConfiguration.metricReader = this.metricReader;
|
|
* }
|
|
*/
|
|
|
|
if (logRecordProcessors.length > 0) {
|
|
(
|
|
nodeSdkConfiguration as opentelemetry.NodeSDKConfiguration & {
|
|
logRecordProcessors?: Array<LogRecordProcessor>;
|
|
}
|
|
).logRecordProcessors = logRecordProcessors;
|
|
}
|
|
|
|
const sdk: opentelemetry.NodeSDK = new opentelemetry.NodeSDK(
|
|
nodeSdkConfiguration,
|
|
);
|
|
|
|
this.getMeterProvider();
|
|
this.getMeter();
|
|
|
|
process.on("SIGTERM", () => {
|
|
sdk.shutdown().finally(() => {
|
|
return process.exit(0);
|
|
});
|
|
});
|
|
|
|
sdk.start();
|
|
|
|
this.sdk = sdk;
|
|
}
|
|
|
|
return this.sdk;
|
|
}
|
|
|
|
public static getLogger(): Logger | null {
|
|
if (!this.loggerProvider) {
|
|
return null;
|
|
}
|
|
|
|
return this.loggerProvider.getLogger("default");
|
|
}
|
|
|
|
public static getMeterProvider(): MeterProvider {
|
|
if (!this.meterProvider) {
|
|
this.meterProvider = new MeterProvider({
|
|
resource: this.getResource({
|
|
serviceName: this.serviceName || "default",
|
|
}),
|
|
readers: this.metricReader ? [this.metricReader] : [],
|
|
});
|
|
|
|
OpenTelemetryAPI.metrics.setGlobalMeterProvider(this.meterProvider);
|
|
}
|
|
|
|
return this.meterProvider;
|
|
}
|
|
|
|
public static getMeter(): Meter {
|
|
if (!this.meter) {
|
|
this.meter = OpenTelemetryAPI.metrics.getMeter("default");
|
|
}
|
|
|
|
return this.meter;
|
|
}
|
|
|
|
public static getCounter(data: {
|
|
name: string;
|
|
description: string;
|
|
unit?: string;
|
|
}): Counter {
|
|
const { name, description } = data;
|
|
|
|
const metricOptions: MetricOptions = {
|
|
description: description,
|
|
};
|
|
|
|
if (data.unit) {
|
|
metricOptions.unit = data.unit;
|
|
}
|
|
|
|
const counter: Counter<opentelemetry.api.Attributes> =
|
|
this.getMeter().createCounter(name, metricOptions);
|
|
|
|
return counter;
|
|
}
|
|
|
|
// guage
|
|
|
|
public static getGauge(data: {
|
|
name: string;
|
|
description: string;
|
|
unit?: string;
|
|
}): opentelemetry.api.UpDownCounter<opentelemetry.api.Attributes> {
|
|
const { name, description } = data;
|
|
|
|
const metricOptions: MetricOptions = {
|
|
description: description,
|
|
};
|
|
|
|
if (data.unit) {
|
|
metricOptions.unit = data.unit;
|
|
}
|
|
|
|
const guage: opentelemetry.api.UpDownCounter<opentelemetry.api.Attributes> =
|
|
this.getMeter().createUpDownCounter(name, metricOptions);
|
|
|
|
return guage;
|
|
}
|
|
|
|
// histogram
|
|
|
|
public static getHistogram(data: {
|
|
name: string;
|
|
description: string;
|
|
unit?: string;
|
|
}): Histogram {
|
|
const { name, description } = data;
|
|
|
|
const metricOptions: MetricOptions = {
|
|
description: description,
|
|
};
|
|
|
|
if (data.unit) {
|
|
metricOptions.unit = data.unit;
|
|
}
|
|
|
|
const histogram: Histogram<opentelemetry.api.Attributes> =
|
|
this.getMeter().createHistogram(name, metricOptions);
|
|
|
|
return histogram;
|
|
}
|
|
|
|
public static getTracer(): opentelemetry.api.Tracer {
|
|
const tracer: opentelemetry.api.Tracer =
|
|
OpenTelemetryAPI.trace.getTracer("default");
|
|
return tracer;
|
|
}
|
|
|
|
public static startActiveSpan<T>(data: {
|
|
name: string;
|
|
options?: SpanOptions | undefined;
|
|
fn: (span: Span) => T;
|
|
}): T {
|
|
const { name } = data;
|
|
|
|
return this.getTracer().startActiveSpan(name, data.options || {}, data.fn);
|
|
}
|
|
|
|
public static recordExceptionMarkSpanAsErrorAndEndSpan(data: {
|
|
span: Span;
|
|
exception: unknown;
|
|
}): void {
|
|
const { span, exception } = data;
|
|
|
|
// log the exception as well
|
|
logger.error(exception);
|
|
|
|
span.recordException(exception as SpanException);
|
|
span.setStatus({
|
|
code: SpanStatusCode.ERROR,
|
|
});
|
|
|
|
this.endSpan(span);
|
|
}
|
|
|
|
public static endSpan(span: Span): void {
|
|
span.end();
|
|
}
|
|
}
|