import { afterEach, describe, expect, test } from "bun:test" import { NodeFileSystem } from "@effect/platform-node" import { Effect, Layer, Logger } from "effect" import fs from "fs/promises" import os from "os" import path from "path" import { fileLogger } from "../../src/observability/logging" import { resource } from "../../src/observability/otlp" const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES const opencodeClient = process.env.OPENCODE_CLIENT afterEach(() => { if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT else process.env.OPENCODE_CLIENT = opencodeClient }) describe("resource", () => { test("parses and decodes OTEL resource attributes", () => { process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,team=platform%2Cobservability,label=hello%3Dworld,key%2Fname=value%20here" expect(resource().attributes).toMatchObject({ "service.namespace": "anomalyco", team: "platform,observability", label: "hello=world", "key/name": "value here", }) }) test("drops OTEL resource attributes when any entry is invalid", () => { process.env.OTEL_RESOURCE_ATTRIBUTES = "service.namespace=anomalyco,broken" expect(resource().attributes["service.namespace"]).toBeUndefined() expect(resource().attributes["opencode.client"]).toBeDefined() }) test("keeps built-in attributes when env values conflict", () => { process.env.OPENCODE_CLIENT = "cli" process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco" expect(resource().attributes).toMatchObject({ "opencode.client": "cli", "service.namespace": "anomalyco", }) expect(resource().attributes["service.instance.id"]).not.toBe("override") expect(resource().attributes["opencode.run"]).toMatch(/^[0-9a-f]{8}$/) }) }) test("file logger appends concurrent runs with a run on every line", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-log-test-")) await using _ = { async [Symbol.asyncDispose]() { await fs.rm(dir, { recursive: true, force: true }) }, } const file = path.join(dir, "opencode.log") const write = (runID: string) => Effect.forEach( Array.from({ length: 50 }, (_, index) => index), (index) => Effect.logInfo(`entry-${index}`), ).pipe( Effect.provide(Logger.layer([fileLogger(file, runID)]).pipe(Layer.provide(NodeFileSystem.layer), Layer.orDie)), Effect.scoped, ) await Effect.runPromise(Effect.all([write("run-a"), write("run-b")], { concurrency: "unbounded" })) const lines = (await Bun.file(file).text()).trim().split("\n") expect(lines).toHaveLength(100) expect(lines.filter((line) => line.includes("run=run-a"))).toHaveLength(50) expect(lines.filter((line) => line.includes("run=run-b"))).toHaveLength(50) expect(lines.every((line) => line.startsWith("timestamp=") && line.includes(" level=INFO "))).toBe(true) expect(lines.every((line) => !line.includes(" fiber="))).toBe(true) expect(lines.every((line) => !line.startsWith("{"))).toBe(true) }) test("file logger flattens nested objects", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-log-test-")) await using _ = { async [Symbol.asyncDispose]() { await fs.rm(dir, { recursive: true, force: true }) }, } const file = path.join(dir, "opencode.log") await Effect.logInfo("request complete", { request: { method: "GET", timing: { duration: 42 } }, tags: ["api", "test"], }).pipe( Effect.annotateLogs({ session: { id: "session-1" } }), Effect.provide(Logger.layer([fileLogger(file, "run-a")]).pipe(Layer.provide(NodeFileSystem.layer), Layer.orDie)), Effect.scoped, Effect.runPromise, ) const line = (await Bun.file(file).text()).trim() expect(line).toContain('message="request complete"') expect(line).toContain("request.method=GET") expect(line).toContain("request.timing.duration=42") expect(line).toContain('tags="[\\\"api\\\",\\\"test\\\"]"') expect(line).toContain("session.id=session-1") expect(line).not.toContain("request={") })