import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { TelemetryService } from "./telemetry.service"; import type { Tracer, Span } from "@opentelemetry/api"; describe("TelemetryService", () => { let service: TelemetryService; let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { originalEnv = { ...process.env }; // Enable tracing by default for tests process.env.OTEL_ENABLED = "true"; process.env.OTEL_SERVICE_NAME = "mosaic-api-test"; process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318/v1/traces"; }); afterEach(async () => { process.env = originalEnv; if (service) { await service.onModuleDestroy(); } }); describe("onModuleInit", () => { it("should initialize the SDK when OTEL_ENABLED is true", async () => { service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); it("should not initialize SDK when OTEL_ENABLED is false", async () => { process.env.OTEL_ENABLED = "false"; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); // Should return noop tracer }); it("should use custom service name from env", async () => { process.env.OTEL_SERVICE_NAME = "custom-service"; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); it("should use default service name when not provided", async () => { delete process.env.OTEL_SERVICE_NAME; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); }); describe("getTracer", () => { beforeEach(async () => { service = new TelemetryService(); await service.onModuleInit(); }); it("should return a tracer instance", () => { const tracer = service.getTracer(); expect(tracer).toBeDefined(); expect(typeof tracer.startSpan).toBe("function"); }); it("should return the same tracer instance on multiple calls", () => { const tracer1 = service.getTracer(); const tracer2 = service.getTracer(); expect(tracer1).toBe(tracer2); }); }); describe("startSpan", () => { beforeEach(async () => { service = new TelemetryService(); await service.onModuleInit(); }); it("should create a span with the given name", () => { const span = service.startSpan("test-span"); expect(span).toBeDefined(); expect(typeof span.end).toBe("function"); span.end(); }); it("should create a span with attributes", () => { const span = service.startSpan("test-span", { attributes: { "test.attribute": "value", }, }); expect(span).toBeDefined(); span.end(); }); it("should create nested spans", () => { const parentSpan = service.startSpan("parent-span"); const childSpan = service.startSpan("child-span"); expect(parentSpan).toBeDefined(); expect(childSpan).toBeDefined(); childSpan.end(); parentSpan.end(); }); }); describe("recordException", () => { let span: Span; beforeEach(async () => { service = new TelemetryService(); await service.onModuleInit(); span = service.startSpan("test-span"); }); afterEach(() => { span.end(); }); it("should record an exception on the span", () => { const error = new Error("Test error"); const recordExceptionSpy = vi.spyOn(span, "recordException"); service.recordException(span, error); expect(recordExceptionSpy).toHaveBeenCalledWith(error); }); it("should set span status to error", () => { const error = new Error("Test error"); const setStatusSpy = vi.spyOn(span, "setStatus"); service.recordException(span, error); expect(setStatusSpy).toHaveBeenCalled(); }); }); describe("onModuleDestroy", () => { it("should shutdown the SDK gracefully", async () => { service = new TelemetryService(); await service.onModuleInit(); await expect(service.onModuleDestroy()).resolves.not.toThrow(); }); it("should not throw if called multiple times", async () => { service = new TelemetryService(); await service.onModuleInit(); await service.onModuleDestroy(); await expect(service.onModuleDestroy()).resolves.not.toThrow(); }); it("should not throw if SDK was not initialized", async () => { process.env.OTEL_ENABLED = "false"; service = new TelemetryService(); await service.onModuleInit(); await expect(service.onModuleDestroy()).resolves.not.toThrow(); }); }); describe("disabled mode", () => { beforeEach(() => { process.env.OTEL_ENABLED = "false"; }); it("should return noop tracer when disabled", async () => { service = new TelemetryService(); await service.onModuleInit(); const tracer = service.getTracer(); expect(tracer).toBeDefined(); }); it("should not throw when creating spans while disabled", async () => { service = new TelemetryService(); await service.onModuleInit(); expect(() => service.startSpan("test-span")).not.toThrow(); }); }); describe("resource attributes", () => { it("should set service.version from package.json", async () => { service = new TelemetryService(); await service.onModuleInit(); // We can't directly assert the resource attributes, but we can verify // the service initializes without error expect(service.getTracer()).toBeDefined(); }); it("should set deployment.environment from NODE_ENV", async () => { process.env.NODE_ENV = "production"; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); it("should default deployment.environment to development", async () => { delete process.env.NODE_ENV; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); }); describe("trace sampling", () => { it("should use default sampling ratio of 1.0 when not configured", async () => { delete process.env.OTEL_TRACES_SAMPLER_ARG; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); it("should respect OTEL_TRACES_SAMPLER_ARG for sampling ratio", async () => { process.env.OTEL_TRACES_SAMPLER_ARG = "0.5"; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); it("should handle invalid sampling ratio gracefully", async () => { process.env.OTEL_TRACES_SAMPLER_ARG = "invalid"; service = new TelemetryService(); await service.onModuleInit(); // Should fall back to default and still work expect(service.getTracer()).toBeDefined(); }); it("should clamp sampling ratio to 0.0-1.0 range", async () => { process.env.OTEL_TRACES_SAMPLER_ARG = "1.5"; service = new TelemetryService(); await service.onModuleInit(); expect(service.getTracer()).toBeDefined(); }); }); });