import os from "os";
import path from "path";
import util from "util";
import { context, trace } from "@opentelemetry/api";
import { SeverityNumber } from "@opentelemetry/api-logs";
import {
BatchLogRecordProcessor,
LoggerProvider,
} from "@opentelemetry/sdk-logs";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
//
//
//
//
// CONFIG --- start ---
const SERVICE_NAME = process.env.OTELIC_SERVICE_NAME || "My Service";
const SERVICE_VERSION = process.env.OTELIC_SERVICE_VERSION || "v1.0";
const APP_NAME = process.env.OTELIC_APP_NAME || "My App";
const API_KEY = process.env.OTELIC_API_KEY || "GET_FROM_WORKSPACE_SETTINGS";
// Default: ["log", "info", "warn", "error", "debug"]
const CONSOLE_METHODS_TO_INSTRUMENT = ["log", "info", "warn", "error", "debug"];
// CONFIG --- end -----
//
//
//
//
//
//
//
//
const traceExporter = new OTLPTraceExporter({
url: "https://ingest.otelic.com/otel/v1/traces",
headers: { "x-api-key": API_KEY },
concurrencyLimit: 3,
});
const logExporter = new OTLPLogExporter({
url: "https://ingest.otelic.com/otel/v1/logs",
headers: { "x-api-key": API_KEY },
concurrencyLimit: 3,
});
const resource = new Resource({
[ATTR_SERVICE_NAME]: SERVICE_NAME,
[ATTR_SERVICE_VERSION]: SERVICE_VERSION,
"app.name": APP_NAME,
"deployment.environment.name": process.env.NODE_ENV || "",
"host.name": os.hostname(),
"os.type": os.type(),
"os.version": os.version(),
});
const loggerProvider = new LoggerProvider({ resource });
const processor = new BatchLogRecordProcessor(logExporter);
loggerProvider.addLogRecordProcessor(processor);
const loggerName = "nodejs";
const loggerVersion = "1.0.0";
const logger = loggerProvider.getLogger(loggerName, loggerVersion);
const tracerProvider = new NodeTracerProvider({
resource,
spanProcessors: [new BatchSpanProcessor(traceExporter)],
});
tracerProvider.register();
// const tracer = tracerProvider.getTracer(APP_NAME);
registerInstrumentations({
instrumentations: [
getNodeAutoInstrumentations({
// load custom configuration for http instrumentation
"@opentelemetry/instrumentation-http": {
startIncomingSpanHook: (request) => {
const method = request.method;
const url = request.url;
return { operationName: `${method} ${url}` };
},
},
"@opentelemetry/instrumentation-mongodb": {
enhancedDatabaseReporting: true,
},
"@opentelemetry/instrumentation-mysql": {
enhancedDatabaseReporting: true,
},
}),
],
});
const sdk = new NodeSDK({ resource, traceExporter });
sdk.start();
export function getLogContext() {
const activeSpan = trace.getSpan(context.active());
const traceId = activeSpan ? activeSpan.spanContext().traceId : "";
const spanId = activeSpan ? activeSpan.spanContext().spanId : "";
const fileName =
typeof require !== "undefined" && require.main?.filename
? path.basename(require.main.filename)
: typeof import.meta !== "undefined" && import.meta.url
? path.basename(new URL(import.meta.url).pathname)
: path.basename(process.argv[1]);
const filePath =
typeof require !== "undefined" && require.main?.filename
? require.main.filename
: typeof import.meta !== "undefined" && import.meta.url
? new URL(import.meta.url).pathname
: process.argv[1];
return {
"app.name": APP_NAME,
"service.name": SERVICE_NAME,
"env.node_env": process.env.NODE_ENV || "development",
"host.name": os.hostname(),
"os.type": os.type(),
"os.version": os.version(),
"file.name": fileName,
"file.path": filePath,
"log.source": "console",
"runtime.name": "nodejs",
"runtime.version": process.version,
"runtime.arch": process.arch,
"process.id": process.pid,
"process.name": process.title,
"process.command": process.argv.join(" "),
"process.uptime": process.uptime(),
traceId,
spanId,
};
}
process.on("uncaughtException", (error) => {
const message = `Uncaught Exception: ${error.message}\n${error.stack}`;
logger.emit({
severityNumber: SeverityNumber.FATAL,
severityText: "fatal",
body: message,
attributes: { "log.source": "uncaughtException" },
});
// Ensure logs are flushed before exiting
processor.shutdown().finally(() => {
console.error(message);
process.exit(1);
});
});
// Map console methods to OpenTelemetry severity levels
const severityMap = {
log: { severityNumber: SeverityNumber.INFO, severityText: "info" },
info: { severityNumber: SeverityNumber.INFO, severityText: "info" },
warn: { severityNumber: SeverityNumber.WARN, severityText: "warn" },
error: { severityNumber: SeverityNumber.ERROR, severityText: "error" },
debug: { severityNumber: SeverityNumber.DEBUG, severityText: "debug" },
};
// Instrument console methods for logs
const originalConsole = { ...console };
CONSOLE_METHODS_TO_INSTRUMENT.map((method) => {
if (method in console) {
originalConsole[method] = console[method];
console[method] = (...args) => {
const { severityNumber, severityText } = severityMap[method];
let _parts = [];
args.forEach((arg) => {
if (arg instanceof Error) {
_parts.push(`${arg.message}\n${arg.stack}`);
} else if (typeof arg === "object") {
try {
_parts.push(JSON.stringify(arg));
} catch (error) {
_parts.push(util.inspect(arg, { depth: null, colors: false }));
}
} else {
_parts.push(arg.toString());
}
});
const message = _parts.join(" ");
// Emit the log to OpenTelemetry
logger.emit({
severityNumber,
severityText,
body: message,
attributes: {
...getLogContext(),
},
});
// Call the original console method to keep standard logs functional
originalConsole[method](...args);
};
}
});
// Graceful shutdown to flush logs and traces before exit
const gracefulShutdown = () => {
Promise.all([processor.shutdown(), tracerProvider.shutdown()]).finally(() => {
process.exit(0);
});
};
// Capture termination signals
process.on("SIGINT", gracefulShutdown);
process.on("SIGTERM", gracefulShutdown);