WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import type { ExportResult } from "@opentelemetry/core";
2import { ExportResultCode } from "@opentelemetry/core";
3import type { LogRecordExporter, ReadableLogRecord } from "@opentelemetry/sdk-logs";
4import { SeverityNumber } from "@opentelemetry/api-logs";
5import {
6 ATTR_SERVICE_NAME,
7 ATTR_SERVICE_VERSION,
8} from "@opentelemetry/semantic-conventions";
9
10const SEVERITY_TEXT: Record<number, string> = {
11 [SeverityNumber.DEBUG]: "debug",
12 [SeverityNumber.DEBUG2]: "debug",
13 [SeverityNumber.DEBUG3]: "debug",
14 [SeverityNumber.DEBUG4]: "debug",
15 [SeverityNumber.INFO]: "info",
16 [SeverityNumber.INFO2]: "info",
17 [SeverityNumber.INFO3]: "info",
18 [SeverityNumber.INFO4]: "info",
19 [SeverityNumber.WARN]: "warn",
20 [SeverityNumber.WARN2]: "warn",
21 [SeverityNumber.WARN3]: "warn",
22 [SeverityNumber.WARN4]: "warn",
23 [SeverityNumber.ERROR]: "error",
24 [SeverityNumber.ERROR2]: "error",
25 [SeverityNumber.ERROR3]: "error",
26 [SeverityNumber.ERROR4]: "error",
27 [SeverityNumber.FATAL]: "fatal",
28 [SeverityNumber.FATAL2]: "fatal",
29 [SeverityNumber.FATAL3]: "fatal",
30 [SeverityNumber.FATAL4]: "fatal",
31};
32
33/**
34 * Exports OTel log records as newline-delimited JSON to stdout.
35 *
36 * Output format:
37 * {"timestamp":"2026-02-23T12:00:00.000Z","level":"info","message":"Server started","service":"atbb-appview","port":3000}
38 *
39 * Compatible with standard log aggregation tools (ELK, Grafana Loki, Datadog, etc.).
40 */
41export class StructuredLogExporter implements LogRecordExporter {
42 export(
43 records: ReadableLogRecord[],
44 resultCallback: (result: ExportResult) => void
45 ): void {
46 try {
47 for (const record of records) {
48 const entry = this.formatRecord(record);
49 process.stdout.write(JSON.stringify(entry) + "\n");
50 }
51 resultCallback({ code: ExportResultCode.SUCCESS });
52 } catch {
53 resultCallback({ code: ExportResultCode.FAILED });
54 }
55 }
56
57 shutdown(): Promise<void> {
58 return Promise.resolve();
59 }
60
61 private formatRecord(record: ReadableLogRecord): Record<string, unknown> {
62 const resourceAttrs = record.resource?.attributes ?? {};
63 const logAttrs = record.attributes ?? {};
64
65 // Build the base log entry
66 const entry: Record<string, unknown> = {
67 timestamp: this.hrTimeToISO(record.hrTime),
68 level: SEVERITY_TEXT[record.severityNumber ?? SeverityNumber.INFO] ?? "info",
69 message: record.body ?? "",
70 };
71
72 // Add service metadata from resource
73 const service = resourceAttrs[ATTR_SERVICE_NAME];
74 if (service) {
75 entry.service = service;
76 }
77 const version = resourceAttrs[ATTR_SERVICE_VERSION];
78 if (version) {
79 entry.version = version;
80 }
81 const environment = resourceAttrs["deployment.environment.name"];
82 if (environment) {
83 entry.environment = environment;
84 }
85
86 // Spread user-provided attributes into the top level
87 for (const [key, value] of Object.entries(logAttrs)) {
88 if (value !== undefined && value !== null) {
89 entry[key] = value;
90 }
91 }
92
93 return entry;
94 }
95
96 private hrTimeToISO(hrTime: [number, number]): string {
97 const ms = hrTime[0] * 1000 + hrTime[1] / 1_000_000;
98 return new Date(ms).toISOString();
99 }
100}