refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {type Attributes, type Counter, type Histogram, type Meter, metrics} from '@opentelemetry/api';
const METER_NAME = 'fluxer.telemetry';
export interface CounterMetric {
name: string;
value?: number;
dimensions?: Record<string, string>;
}
export interface HistogramMetric {
name: string;
valueMs: number;
dimensions?: Record<string, string>;
}
export interface GaugeMetric {
name: string;
value: number;
dimensions?: Record<string, string>;
}
interface IMetricRegistry {
getCounter(name: string, description?: string): Counter<Attributes>;
getHistogram(name: string, description?: string): Histogram<Attributes>;
}
class TelemetryMetricRegistry implements IMetricRegistry {
private readonly counters = new Map<string, Counter<Attributes>>();
private readonly histograms = new Map<string, Histogram<Attributes>>();
private readonly meter: Meter;
public constructor() {
this.meter = metrics.getMeter(METER_NAME);
}
public getCounter(name: string, description?: string): Counter<Attributes> {
const existingCounter = this.counters.get(name);
if (existingCounter !== undefined) {
return existingCounter;
}
const counter = this.meter.createCounter(name, {
description: description ?? `Counter for ${name}`,
});
this.counters.set(name, counter);
return counter;
}
public getHistogram(name: string, description?: string): Histogram<Attributes> {
const existingHistogram = this.histograms.get(name);
if (existingHistogram !== undefined) {
return existingHistogram;
}
const histogram = this.meter.createHistogram(name, {
description: description ?? `Histogram for ${name}`,
});
this.histograms.set(name, histogram);
return histogram;
}
}
const metricRegistry = new TelemetryMetricRegistry();
function toAttributes(dimensions?: Record<string, string>): Attributes {
if (dimensions === undefined) {
return {};
}
return {...dimensions};
}
export function createCounter(name: string, description?: string): Counter<Attributes> {
return metricRegistry.getCounter(name, description);
}
export function createHistogram(name: string, description?: string): Histogram<Attributes> {
return metricRegistry.getHistogram(name, description);
}
export function recordCounter(name: string, value?: number, attributes?: Attributes): void;
export function recordCounter(metric: CounterMetric): void;
export function recordCounter(nameOrMetric: string | CounterMetric, value: number = 1, attributes?: Attributes): void {
if (typeof nameOrMetric === 'string') {
createCounter(nameOrMetric).add(value, attributes);
return;
}
createCounter(nameOrMetric.name).add(nameOrMetric.value ?? 1, toAttributes(nameOrMetric.dimensions));
}
export function recordHistogram(name: string, value: number, attributes?: Attributes): void;
export function recordHistogram(metric: HistogramMetric): void;
export function recordHistogram(nameOrMetric: string | HistogramMetric, value?: number, attributes?: Attributes): void {
if (typeof nameOrMetric === 'string') {
if (value === undefined) {
throw new Error(`Histogram metric "${nameOrMetric}" requires a numeric value`);
}
createHistogram(nameOrMetric).record(value, attributes);
return;
}
createHistogram(nameOrMetric.name).record(nameOrMetric.valueMs, toAttributes(nameOrMetric.dimensions));
}
export function recordGauge(metric: GaugeMetric): void {
createHistogram(metric.name).record(metric.value, toAttributes(metric.dimensions));
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {getDefaultTelemetryConfig} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryConfig';
import {
createDefaultTelemetryLogger,
type ITelemetryInitLogger,
} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryLogger';
import {
createTelemetryRuntimeManager,
type ITelemetryRuntimeManager,
} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryManager';
export interface InstrumentationConfig {
cassandra?: boolean;
aws?: boolean;
fetch?: boolean;
}
export interface TelemetryConfig {
enabled: boolean;
serviceName: string;
serviceVersion: string;
serviceInstanceId: string;
environment: string;
traceSamplingRatio: number;
otlpEndpoint: string;
otlpApiKey?: string;
exportTimeout: number;
metricExportIntervalMs: number;
ignoreIncomingPaths: Array<string>;
instrumentations?: InstrumentationConfig;
}
export interface OtlpConfig {
endpoints: {
traces: string;
metrics: string;
logs: string;
};
headers?: Record<string, string>;
}
export interface TelemetryInitLogger {
info(msg: string): void;
info(obj: Record<string, unknown>, msg: string): void;
}
export interface ITelemetryManager {
initialize(config?: Partial<TelemetryConfig>): Promise<void>;
shutdown(): Promise<void>;
isActive(): boolean;
shouldInitialize(): boolean;
getConfig(): TelemetryConfig | null;
getOtlpConfig(): OtlpConfig | null;
}
export interface TelemetryManagerOptions {
logger?: TelemetryInitLogger;
shouldInitialize?: () => boolean;
}
function createRuntimeManager(options?: TelemetryManagerOptions): ITelemetryRuntimeManager {
const logger: ITelemetryInitLogger = options?.logger ?? createDefaultTelemetryLogger();
return createTelemetryRuntimeManager({
logger,
shouldInitialize: options?.shouldInitialize ?? (() => true),
});
}
function createManagerFacade(runtimeManager: ITelemetryRuntimeManager): ITelemetryManager {
return {
initialize(config?: Partial<TelemetryConfig>): Promise<void> {
return runtimeManager.initialize(config);
},
shutdown(): Promise<void> {
return runtimeManager.shutdown();
},
isActive(): boolean {
return runtimeManager.isActive();
},
shouldInitialize(): boolean {
return runtimeManager.shouldInitialize();
},
getConfig(): TelemetryConfig | null {
return runtimeManager.getConfig();
},
getOtlpConfig(): OtlpConfig | null {
return runtimeManager.getOtlpConfig();
},
};
}
export function createTelemetryManager(options?: TelemetryManagerOptions): ITelemetryManager {
return createManagerFacade(createRuntimeManager(options));
}
const defaultTelemetryManager = createTelemetryManager();
export function isTelemetryActive(): boolean {
return defaultTelemetryManager.isActive();
}
export function getDefaultConfig(): TelemetryConfig {
return getDefaultTelemetryConfig();
}
export function shouldInitializeTelemetry(): boolean {
return defaultTelemetryManager.shouldInitialize();
}
export async function initializeTelemetry(config?: Partial<TelemetryConfig>): Promise<void> {
await defaultTelemetryManager.initialize(config);
}
export async function shutdownTelemetry(): Promise<void> {
await defaultTelemetryManager.shutdown();
}
export function getTelemetryConfig(): TelemetryConfig | null {
return defaultTelemetryManager.getConfig();
}
export function getOtlpConfig(): OtlpConfig | null {
return defaultTelemetryManager.getOtlpConfig();
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {Attributes, Span} from '@opentelemetry/api';
import {SpanStatusCode, trace} from '@opentelemetry/api';
const DEFAULT_TRACER_NAME = 'fluxer';
export interface WithSpanOptions {
attributes?: Attributes;
tracerName?: string;
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
export function getTracer(name: string = DEFAULT_TRACER_NAME) {
return trace.getTracer(name);
}
export async function withSpan<T>(
name: string,
fn: (span: Span) => Promise<T> | T,
options?: WithSpanOptions,
): Promise<T> {
const tracer = getTracer(options?.tracerName);
const spanOptions = options?.attributes === undefined ? {} : {attributes: options.attributes};
return tracer.startActiveSpan(name, spanOptions, async (span) => {
try {
const result = await fn(span);
span.setStatus({code: SpanStatusCode.OK});
return result;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: getErrorMessage(error),
});
throw error;
} finally {
span.end();
}
}) as Promise<T>;
}
export function addSpanEvent(name: string, attributes?: Attributes): void {
const activeSpan = trace.getActiveSpan();
if (activeSpan !== undefined) {
activeSpan.addEvent(name, attributes);
}
}
export function setSpanAttributes(attributes: Attributes): void {
const activeSpan = trace.getActiveSpan();
if (activeSpan !== undefined) {
activeSpan.setAttributes(attributes);
}
}
export function getActiveSpan(): Span | undefined {
return trace.getActiveSpan();
}
export function formatTraceparent(span: Span): string | null {
const spanContext = span.spanContext();
if (!spanContext.traceId || !spanContext.spanId) {
return null;
}
const traceFlags = spanContext.traceFlags?.toString(16).padStart(2, '0') ?? '00';
return `00-${spanContext.traceId}-${spanContext.spanId}-${traceFlags}`;
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {hostname} from 'node:os';
import type {OtlpConfig, TelemetryConfig} from '@fluxer/telemetry/src/Telemetry';
function assertPositiveNumber(value: number, fieldName: string): void {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`Telemetry config "${fieldName}" must be a positive number`);
}
}
function normalizeEndpoint(endpoint: string): string {
const trimmedEndpoint = endpoint.trim();
return trimmedEndpoint.replace(/\/+$/, '');
}
function normalizeIgnoreIncomingPaths(paths: Array<string>): Array<string> {
const pathSet = new Set<string>();
for (const path of paths) {
const normalizedPath = path.trim();
if (normalizedPath.length === 0) {
continue;
}
pathSet.add(normalizedPath);
}
return [...pathSet];
}
function validateTelemetryConfig(config: TelemetryConfig): void {
if (config.serviceName.trim().length === 0) {
throw new Error('Telemetry config "serviceName" must not be empty');
}
if (config.serviceVersion.trim().length === 0) {
throw new Error('Telemetry config "serviceVersion" must not be empty');
}
if (config.environment.trim().length === 0) {
throw new Error('Telemetry config "environment" must not be empty');
}
if (config.enabled && config.otlpEndpoint.length === 0) {
throw new Error('Telemetry config "otlpEndpoint" must not be empty when telemetry is enabled');
}
if (!Number.isFinite(config.traceSamplingRatio) || config.traceSamplingRatio < 0 || config.traceSamplingRatio > 1) {
throw new Error('Telemetry config "traceSamplingRatio" must be between 0 and 1');
}
assertPositiveNumber(config.exportTimeout, 'exportTimeout');
assertPositiveNumber(config.metricExportIntervalMs, 'metricExportIntervalMs');
}
export function getDefaultTelemetryConfig(): TelemetryConfig {
return {
enabled: false,
serviceName: 'fluxer',
serviceVersion: 'unknown',
serviceInstanceId: hostname(),
environment: 'development',
traceSamplingRatio: 1.0,
otlpEndpoint: '',
exportTimeout: 30000,
metricExportIntervalMs: 60000,
ignoreIncomingPaths: ['/_health'],
};
}
export function resolveTelemetryConfig(config?: Partial<TelemetryConfig>): TelemetryConfig {
const defaults = getDefaultTelemetryConfig();
const defined = Object.fromEntries(
Object.entries(config ?? {}).filter(([, v]) => v !== undefined),
) as Partial<TelemetryConfig>;
const mergedConfig: TelemetryConfig = {
...defaults,
...defined,
otlpEndpoint: normalizeEndpoint(config?.otlpEndpoint ?? defaults.otlpEndpoint),
ignoreIncomingPaths: normalizeIgnoreIncomingPaths(config?.ignoreIncomingPaths ?? defaults.ignoreIncomingPaths),
instrumentations: config?.instrumentations === undefined ? defaults.instrumentations : {...config.instrumentations},
};
validateTelemetryConfig(mergedConfig);
return mergedConfig;
}
export function cloneTelemetryConfig(config: TelemetryConfig): TelemetryConfig {
return {
...config,
ignoreIncomingPaths: [...config.ignoreIncomingPaths],
instrumentations: config.instrumentations === undefined ? undefined : {...config.instrumentations},
};
}
export function createOtlpConfigFromTelemetryConfig(config: TelemetryConfig): OtlpConfig {
const baseEndpoint = config.otlpEndpoint.replace(/\/+$/, '');
const headers = config.otlpApiKey === undefined ? undefined : {Authorization: `Bearer ${config.otlpApiKey}`};
return {
endpoints: {
traces: `${baseEndpoint}/v1/traces`,
metrics: `${baseEndpoint}/v1/metrics`,
logs: `${baseEndpoint}/v1/logs`,
},
...(headers !== undefined && {headers}),
};
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {InstrumentationConfig, TelemetryConfig} from '@fluxer/telemetry/src/Telemetry';
import type {ITelemetryInitLogger} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryLogger';
import type {Instrumentation} from '@opentelemetry/instrumentation';
import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';
import {PinoInstrumentation} from '@opentelemetry/instrumentation-pino';
interface InstrumentationModule {
[key: string]: new (...args: Array<unknown>) => Instrumentation;
}
interface OptionalInstrumentationDefinition {
enabled: boolean;
moduleName: string;
className: string;
config?: Record<string, unknown>;
}
function formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function getOptionalInstrumentationDefinitions(
config: InstrumentationConfig,
): Array<OptionalInstrumentationDefinition> {
return [
{
enabled: config.fetch !== false,
moduleName: '@opentelemetry/instrumentation-fetch',
className: 'FetchInstrumentation',
},
{
enabled: config.cassandra === true,
moduleName: '@opentelemetry/instrumentation-cassandra-driver',
className: 'CassandraDriverInstrumentation',
},
{
enabled: config.aws === true,
moduleName: '@opentelemetry/instrumentation-aws-sdk',
className: 'AwsInstrumentation',
config: {suppressInternalInstrumentation: true},
},
];
}
async function loadOptionalInstrumentation(
definition: OptionalInstrumentationDefinition,
logger: ITelemetryInitLogger,
): Promise<Instrumentation | null> {
try {
const module = (await import(definition.moduleName)) as InstrumentationModule;
const InstrumentationClass = module[definition.className];
if (typeof InstrumentationClass !== 'function') {
logger.info(
{moduleName: definition.moduleName, className: definition.className},
'Optional instrumentation class not found',
);
return null;
}
if (definition.config === undefined) {
return new (InstrumentationClass as new () => Instrumentation)();
}
return new (InstrumentationClass as new (config: Record<string, unknown>) => Instrumentation)(definition.config);
} catch (error) {
logger.info(
{moduleName: definition.moduleName, className: definition.className, error: formatError(error)},
'Optional instrumentation not available',
);
return null;
}
}
export async function createTelemetryInstrumentations(
config: TelemetryConfig,
logger: ITelemetryInitLogger,
): Promise<Array<Instrumentation>> {
const ignoredIncomingPaths = new Set(config.ignoreIncomingPaths);
const instrumentations: Array<Instrumentation> = [
new HttpInstrumentation({
ignoreIncomingRequestHook: (request) => {
const path = request.url?.split('?')[0];
if (path === undefined) {
return false;
}
return ignoredIncomingPaths.has(path);
},
}),
new PinoInstrumentation(),
];
const optionalInstrumentationConfig = config.instrumentations ?? {};
const optionalInstrumentationDefinitions = getOptionalInstrumentationDefinitions(optionalInstrumentationConfig);
for (const definition of optionalInstrumentationDefinitions) {
if (!definition.enabled) {
continue;
}
const instrumentation = await loadOptionalInstrumentation(definition, logger);
if (instrumentation !== null) {
instrumentations.push(instrumentation);
}
}
return instrumentations;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
export interface ITelemetryInitLogger {
info(msg: string): void;
info(obj: Record<string, unknown>, msg: string): void;
}
export function createDefaultTelemetryLogger(): ITelemetryInitLogger {
return {
info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
if (typeof objOrMsg === 'string') {
process.stdout.write(`[telemetry] ${objOrMsg}\n`);
return;
}
if (msg !== undefined) {
process.stdout.write(`[telemetry] ${msg} ${JSON.stringify(objOrMsg)}\n`);
}
},
};
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {OtlpConfig, TelemetryConfig} from '@fluxer/telemetry/src/Telemetry';
import {
cloneTelemetryConfig,
createOtlpConfigFromTelemetryConfig,
getDefaultTelemetryConfig,
resolveTelemetryConfig,
} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryConfig';
import type {ITelemetryInitLogger} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryLogger';
import {createTelemetrySdk} from '@fluxer/telemetry/src/telemetry_runtime/TelemetrySdk';
import type {NodeSDK} from '@opentelemetry/sdk-node';
export interface ITelemetryRuntimeManager {
initialize(config?: Partial<TelemetryConfig>): Promise<void>;
shutdown(): Promise<void>;
isActive(): boolean;
shouldInitialize(): boolean;
getConfig(): TelemetryConfig | null;
getOtlpConfig(): OtlpConfig | null;
}
export interface TelemetryRuntimeManagerOptions {
logger: ITelemetryInitLogger;
shouldInitialize: () => boolean;
}
function formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
class TelemetryRuntimeManager implements ITelemetryRuntimeManager {
private sdk: NodeSDK | null = null;
private config: TelemetryConfig | null = null;
private initializationTask: Promise<void> | null = null;
private shutdownTask: Promise<void> | null = null;
private readonly logger: ITelemetryInitLogger;
private readonly shouldInitializePredicate: () => boolean;
public constructor(options: TelemetryRuntimeManagerOptions) {
this.logger = options.logger;
this.shouldInitializePredicate = options.shouldInitialize;
}
public isActive(): boolean {
return this.sdk !== null;
}
public shouldInitialize(): boolean {
return this.shouldInitializePredicate();
}
public getConfig(): TelemetryConfig | null {
if (this.config === null) {
return null;
}
return cloneTelemetryConfig(this.config);
}
public getOtlpConfig(): OtlpConfig | null {
if (!this.shouldInitialize()) {
return null;
}
const config = this.config ?? getDefaultTelemetryConfig();
return createOtlpConfigFromTelemetryConfig(config);
}
public async initialize(config?: Partial<TelemetryConfig>): Promise<void> {
if (this.shutdownTask !== null) {
await this.shutdownTask;
}
if (this.sdk !== null) {
this.logger.info('Telemetry already initialized');
return;
}
if (this.initializationTask !== null) {
await this.initializationTask;
return;
}
if (!this.shouldInitialize()) {
this.logger.info('Telemetry disabled');
return;
}
const resolvedConfig = resolveTelemetryConfig(config);
if (!resolvedConfig.enabled) {
this.config = resolvedConfig;
this.logger.info('Telemetry disabled by configuration');
return;
}
const initializationTask = this.initializeInternal(resolvedConfig);
this.initializationTask = initializationTask;
try {
await initializationTask;
} finally {
if (this.initializationTask === initializationTask) {
this.initializationTask = null;
}
}
}
public async shutdown(): Promise<void> {
if (this.initializationTask !== null) {
await this.initializationTask;
}
if (this.shutdownTask !== null) {
await this.shutdownTask;
return;
}
const shutdownTask = this.shutdownInternal();
this.shutdownTask = shutdownTask;
try {
await shutdownTask;
} finally {
if (this.shutdownTask === shutdownTask) {
this.shutdownTask = null;
}
}
}
private async initializeInternal(config: TelemetryConfig): Promise<void> {
this.logger.info(
{
serviceName: config.serviceName,
environment: config.environment,
endpoint: config.otlpEndpoint,
traceSamplingRatio: config.traceSamplingRatio,
},
'Initializing telemetry',
);
const sdk = await createTelemetrySdk(config, this.logger);
try {
await Promise.resolve(sdk.start());
} catch (error) {
await Promise.resolve(sdk.shutdown()).catch(() => undefined);
this.logger.info({error: formatError(error)}, 'Telemetry initialization failed');
throw error;
}
this.sdk = sdk;
this.config = cloneTelemetryConfig(config);
this.logger.info('Telemetry initialized successfully');
}
private async shutdownInternal(): Promise<void> {
const sdk = this.sdk;
this.sdk = null;
this.config = null;
if (sdk !== null) {
await sdk.shutdown();
}
this.logger.info('Telemetry shutdown complete');
}
}
export function createTelemetryRuntimeManager(options: TelemetryRuntimeManagerOptions): ITelemetryRuntimeManager {
return new TelemetryRuntimeManager(options);
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {TelemetryConfig} from '@fluxer/telemetry/src/Telemetry';
import {createOtlpConfigFromTelemetryConfig} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryConfig';
import {createTelemetryInstrumentations} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryInstrumentations';
import type {ITelemetryInitLogger} from '@fluxer/telemetry/src/telemetry_runtime/TelemetryLogger';
import {OTLPLogExporter} from '@opentelemetry/exporter-logs-otlp-http';
import {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-http';
import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http';
import {resourceFromAttributes} from '@opentelemetry/resources';
import {BatchLogRecordProcessor} from '@opentelemetry/sdk-logs';
import {PeriodicExportingMetricReader} from '@opentelemetry/sdk-metrics';
import {NodeSDK} from '@opentelemetry/sdk-node';
import {ParentBasedSampler, TraceIdRatioBasedSampler} from '@opentelemetry/sdk-trace-base';
import {
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_INSTANCE_ID,
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_NAMESPACE,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
export async function createTelemetrySdk(config: TelemetryConfig, logger: ITelemetryInitLogger): Promise<NodeSDK> {
const otlpConfig = createOtlpConfigFromTelemetryConfig(config);
const resource = resourceFromAttributes({
[SEMRESATTRS_SERVICE_NAME]: config.serviceName,
[SEMRESATTRS_SERVICE_NAMESPACE]: 'fluxer',
[SEMRESATTRS_SERVICE_VERSION]: config.serviceVersion,
[SEMRESATTRS_SERVICE_INSTANCE_ID]: config.serviceInstanceId,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: config.environment,
});
const traceExporter = new OTLPTraceExporter({
url: otlpConfig.endpoints.traces,
...(otlpConfig.headers !== undefined && {headers: otlpConfig.headers}),
timeoutMillis: config.exportTimeout,
});
const metricExporter = new OTLPMetricExporter({
url: otlpConfig.endpoints.metrics,
...(otlpConfig.headers !== undefined && {headers: otlpConfig.headers}),
timeoutMillis: config.exportTimeout,
});
const logExporter = new OTLPLogExporter({
url: otlpConfig.endpoints.logs,
...(otlpConfig.headers !== undefined && {headers: otlpConfig.headers}),
timeoutMillis: config.exportTimeout,
});
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(config.traceSamplingRatio),
});
const instrumentations = await createTelemetryInstrumentations(config, logger);
return new NodeSDK({
resource,
traceExporter,
sampler,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: config.metricExportIntervalMs,
}),
logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
instrumentations,
});
}