loggily 0.0.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * File writer for loggily — Node.js/Bun only.
3
+ *
4
+ * Separated from core logger to allow tree-shaking in browser bundles.
5
+ * Uses dynamic import("node:fs") to avoid static dependency on Node APIs.
6
+ */
7
+
8
+ import { openSync, writeSync, closeSync } from "node:fs"
9
+
10
+ /** Options for creating an async buffered file writer */
11
+ export interface FileWriterOptions {
12
+ /** Buffer size threshold in bytes before flushing (default: 4096) */
13
+ bufferSize?: number
14
+ /** Flush interval in milliseconds (default: 100) */
15
+ flushInterval?: number
16
+ }
17
+
18
+ /** An async buffered file writer with automatic flushing */
19
+ export interface FileWriter {
20
+ /** Write a line to the buffer (appends newline) */
21
+ write(line: string): void
22
+ /** Flush the buffer immediately */
23
+ flush(): void
24
+ /** Close the writer and flush remaining buffer */
25
+ close(): void
26
+ }
27
+
28
+ /**
29
+ * Create an async buffered file writer for log output.
30
+ * Buffers writes and flushes on size threshold or interval.
31
+ * Registers a process.on('exit') handler to flush remaining buffer.
32
+ *
33
+ * **Node.js/Bun only** — not available in browser environments.
34
+ *
35
+ * @param filePath - Path to the log file (opened in append mode)
36
+ * @param options - Buffer size and flush interval configuration
37
+ * @returns FileWriter with write, flush, and close methods
38
+ *
39
+ * @example
40
+ * const writer = createFileWriter('/tmp/app.log')
41
+ * const unsubscribe = addWriter((formatted) => writer.write(formatted))
42
+ *
43
+ * // On shutdown:
44
+ * unsubscribe()
45
+ * writer.close()
46
+ */
47
+ export function createFileWriter(filePath: string, options: FileWriterOptions = {}): FileWriter {
48
+ const bufferSize = options.bufferSize ?? 4096
49
+ const flushInterval = options.flushInterval ?? 100
50
+
51
+ let buffer = ""
52
+ let fd: number | null = null
53
+ let timer: ReturnType<typeof setInterval> | null = null
54
+ let closed = false
55
+
56
+ // Open file in append mode
57
+ fd = openSync(filePath, "a")
58
+
59
+ /** Flush buffer contents to disk synchronously */
60
+ function flush(): void {
61
+ if (buffer.length === 0 || fd === null) return
62
+ const data = buffer
63
+ writeSync(fd, data)
64
+ buffer = ""
65
+ }
66
+
67
+ // Set up periodic flush
68
+ timer = setInterval(flush, flushInterval)
69
+ // Don't let the timer keep the process alive
70
+ if (timer && typeof timer === "object" && "unref" in timer) {
71
+ ;(timer as { unref(): void }).unref()
72
+ }
73
+
74
+ // Flush on process exit to avoid data loss
75
+ const exitHandler = (): void => flush()
76
+ process.on("exit", exitHandler)
77
+
78
+ return {
79
+ write(line: string): void {
80
+ if (closed) return
81
+ buffer += line + "\n"
82
+ if (buffer.length >= bufferSize) {
83
+ flush()
84
+ }
85
+ },
86
+
87
+ flush,
88
+
89
+ close(): void {
90
+ if (closed) return
91
+ closed = true
92
+ if (timer !== null) {
93
+ clearInterval(timer)
94
+ timer = null
95
+ }
96
+ try {
97
+ flush()
98
+ } catch {
99
+ // Swallow flush errors during close — data loss is unavoidable
100
+ // at this point, but we must still release the fd and exit handler.
101
+ } finally {
102
+ if (fd !== null) {
103
+ closeSync(fd)
104
+ fd = null
105
+ }
106
+ process.removeListener("exit", exitHandler)
107
+ }
108
+ },
109
+ }
110
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * loggily browser entry point.
3
+ *
4
+ * Re-exports the full logger API except createFileWriter (which requires node:fs).
5
+ * Bundlers resolve this via the "browser" condition in package.json exports.
6
+ */
7
+
8
+ // Re-export everything from core logger
9
+ export {
10
+ // Types
11
+ type OutputLogLevel,
12
+ type LogLevel,
13
+ type LazyMessage,
14
+ type SpanData,
15
+ type Logger,
16
+ type SpanLogger,
17
+ type OutputMode,
18
+ type LogFormat,
19
+ type ConditionalLogger,
20
+
21
+ // Writers
22
+ addWriter,
23
+ setSuppressConsole,
24
+ setOutputMode,
25
+ getOutputMode,
26
+
27
+ // Configuration
28
+ setLogLevel,
29
+ getLogLevel,
30
+ enableSpans,
31
+ disableSpans,
32
+ spansAreEnabled,
33
+ setTraceFilter,
34
+ getTraceFilter,
35
+ setDebugFilter,
36
+ getDebugFilter,
37
+ setLogFormat,
38
+ getLogFormat,
39
+
40
+ // ID management
41
+ resetIds,
42
+
43
+ // Span collection
44
+ startCollecting,
45
+ stopCollecting,
46
+ getCollectedSpans,
47
+ clearCollectedSpans,
48
+
49
+ // Logger creation
50
+ createLogger,
51
+ } from "./core.js"
52
+
53
+ // Tracing utilities (runtime-agnostic, work in browser)
54
+ export {
55
+ setIdFormat,
56
+ getIdFormat,
57
+ type IdFormat,
58
+ traceparent,
59
+ type TraceparentOptions,
60
+ setSampleRate,
61
+ getSampleRate,
62
+ } from "./tracing.js"
63
+
64
+ // File writer types (exported for type compatibility, but the function throws)
65
+ export type { FileWriterOptions, FileWriter } from "./file-writer.js"
66
+
67
+ /** @throws Always — createFileWriter is not available in browser environments */
68
+ export function createFileWriter(): never {
69
+ throw new Error(
70
+ "createFileWriter is not available in browser environments. Use addWriter() with a custom transport instead.",
71
+ )
72
+ }
package/src/index.ts CHANGED
@@ -1 +1,18 @@
1
- export const VERSION = "0.0.1"
1
+ /**
2
+ * loggily - Structured logging with spans
3
+ *
4
+ * Full entry point for Node.js, Bun, and Deno.
5
+ * Browser environments use index.browser.ts via the "browser" export condition.
6
+ */
7
+
8
+ export * from "./core.js"
9
+ export { createFileWriter, type FileWriter, type FileWriterOptions } from "./file-writer.js"
10
+ export {
11
+ setIdFormat,
12
+ getIdFormat,
13
+ type IdFormat,
14
+ traceparent,
15
+ type TraceparentOptions,
16
+ setSampleRate,
17
+ getSampleRate,
18
+ } from "./tracing.js"
package/src/tracing.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Distributed tracing utilities for loggily.
3
+ *
4
+ * Provides W3C-compatible trace/span ID generation, traceparent header formatting,
5
+ * and head-based sampling. All features are opt-in and don't break the existing API.
6
+ */
7
+
8
+ import type { SpanData } from "./core.js"
9
+
10
+ // ============ ID Format ============
11
+
12
+ /** Supported ID formats */
13
+ export type IdFormat = "simple" | "w3c"
14
+
15
+ let currentIdFormat: IdFormat = "simple"
16
+
17
+ /**
18
+ * Set the ID format for new spans and traces.
19
+ * - "simple": sp_1, sp_2, tr_1, tr_2 (default, lightweight)
20
+ * - "w3c": 32-char hex trace ID, 16-char hex span ID (W3C Trace Context compatible)
21
+ */
22
+ export function setIdFormat(format: IdFormat): void {
23
+ currentIdFormat = format
24
+ }
25
+
26
+ /** Get the current ID format */
27
+ export function getIdFormat(): IdFormat {
28
+ return currentIdFormat
29
+ }
30
+
31
+ // Simple format counters (used by core.ts via the generator functions)
32
+ let simpleSpanCounter = 0
33
+ let simpleTraceCounter = 0
34
+
35
+ /** Generate a hex string of the given byte length using crypto.randomUUID */
36
+ function randomHex(bytes: number): string {
37
+ // crypto.randomUUID() gives us 32 hex chars (128 bits) after removing dashes
38
+ // For 16 bytes (32 hex chars) we use one UUID, for 8 bytes (16 hex chars) we take a slice
39
+ const uuid = crypto.randomUUID().replace(/-/g, "")
40
+ return uuid.slice(0, bytes * 2)
41
+ }
42
+
43
+ /** Generate a span ID according to the current format */
44
+ export function generateSpanId(): string {
45
+ if (currentIdFormat === "w3c") {
46
+ return randomHex(8) // 16-char hex
47
+ }
48
+ return `sp_${(++simpleSpanCounter).toString(36)}`
49
+ }
50
+
51
+ /** Generate a trace ID according to the current format */
52
+ export function generateTraceId(): string {
53
+ if (currentIdFormat === "w3c") {
54
+ return randomHex(16) // 32-char hex
55
+ }
56
+ return `tr_${(++simpleTraceCounter).toString(36)}`
57
+ }
58
+
59
+ /** Reset ID counters (for testing) */
60
+ export function resetIdCounters(): void {
61
+ simpleSpanCounter = 0
62
+ simpleTraceCounter = 0
63
+ }
64
+
65
+ // ============ W3C Traceparent ============
66
+
67
+ /** Options for traceparent header formatting */
68
+ export interface TraceparentOptions {
69
+ /** Whether this span is sampled. Defaults to true for backwards compatibility. */
70
+ sampled?: boolean
71
+ }
72
+
73
+ /**
74
+ * Format a W3C traceparent header from span data.
75
+ *
76
+ * Format: `{version}-{trace-id}-{span-id}-{trace-flags}`
77
+ * - version: "00" (current W3C spec version)
78
+ * - trace-id: 32 hex chars (128 bits)
79
+ * - span-id: 16 hex chars (64 bits)
80
+ * - trace-flags: "01" (sampled) or "00" (not sampled)
81
+ *
82
+ * Works with both simple and W3C ID formats. Simple IDs are zero-padded to spec length.
83
+ *
84
+ * @param spanData - Span data with id and traceId
85
+ * @param options - Optional settings (sampled flag). Defaults to sampled=true.
86
+ * @returns W3C traceparent header string
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const span = log.span("http-request")
91
+ * const header = traceparent(span.spanData)
92
+ * // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
93
+ * fetch(url, { headers: { traceparent: header } })
94
+ * ```
95
+ */
96
+ export function traceparent(spanData: SpanData, options?: TraceparentOptions): string {
97
+ const traceId = padHex(spanData.traceId, 32)
98
+ const spanId = padHex(spanData.id, 16)
99
+ const flags = (options?.sampled ?? true) ? "01" : "00"
100
+ return `00-${traceId}-${spanId}-${flags}`
101
+ }
102
+
103
+ /** Pad or hash an ID to the specified hex length */
104
+ function padHex(id: string, length: number): string {
105
+ // If it's already the right length and looks like hex, use as-is
106
+ if (id.length === length && /^[0-9a-f]+$/.test(id)) {
107
+ return id
108
+ }
109
+
110
+ // For simple IDs (sp_1, tr_1), create a deterministic hex representation
111
+ // by encoding the string as hex bytes, zero-padded to the target length
112
+ let hex = ""
113
+ for (let i = 0; i < id.length; i++) {
114
+ hex += id.charCodeAt(i).toString(16).padStart(2, "0")
115
+ }
116
+ // Pad or truncate to target length
117
+ return hex.padStart(length, "0").slice(-length)
118
+ }
119
+
120
+ // ============ Sampling ============
121
+
122
+ let sampleRate = 1.0
123
+
124
+ /**
125
+ * Set the head-based sampling rate for new traces.
126
+ * Applied at trace creation — all spans within a sampled trace are kept.
127
+ *
128
+ * @param rate - Sampling rate from 0.0 (sample nothing) to 1.0 (sample everything, default)
129
+ */
130
+ export function setSampleRate(rate: number): void {
131
+ if (rate < 0 || rate > 1) {
132
+ throw new Error(`Sample rate must be between 0.0 and 1.0, got ${rate}`)
133
+ }
134
+ sampleRate = rate
135
+ }
136
+
137
+ /** Get the current sampling rate */
138
+ export function getSampleRate(): number {
139
+ return sampleRate
140
+ }
141
+
142
+ /**
143
+ * Determine whether a new trace should be sampled.
144
+ * Called at trace creation time (head-based sampling).
145
+ */
146
+ export function shouldSample(): boolean {
147
+ if (sampleRate >= 1.0) return true
148
+ if (sampleRate <= 0.0) return false
149
+ return Math.random() < sampleRate
150
+ }