@remote-logger/sdk 0.1.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.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Remote Logger SDK
2
+
3
+ A lightweight logging SDK that sends structured logs to a Remote Logger backend via HTTP or WebSocket, while echoing to the local console.
4
+
5
+ ## Quick Start
6
+
7
+ ```ts
8
+ import { createLogger } from '@remote-logger/sdk';
9
+
10
+ const logger = createLogger({
11
+ logId: 'your-log-file-id',
12
+ apiKey: 'your-api-key',
13
+ });
14
+
15
+ logger.info("server started", { port: 3000 });
16
+ logger.warn("slow query", { duration: 1200, query: "SELECT ..." });
17
+ logger.error("payment failed", { orderId: "abc-123" });
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ ```ts
23
+ const logger = createLogger({
24
+ // Required
25
+ logId: 'uuid', // Log file ID from the dashboard
26
+ apiKey: 'your-api-key', // API key for ingestion auth
27
+
28
+ // Optional
29
+ group: 'api', // Default group for all entries
30
+ traceId: 'req-123', // Default trace ID
31
+ silent: false, // Set true to suppress console output (default: false)
32
+ level: 'DEBUG', // Only send logs at this level and above (default: 'DEBUG')
33
+ onError: (err, logs) => { // Custom error handler for flush failures
34
+ // handle error
35
+ },
36
+ });
37
+ ```
38
+
39
+ ### `level` option
40
+
41
+ Controls which logs are sent to the remote service. Defaults to `'DEBUG'` (send everything). Logs below this level are silently dropped — they won't be sent remotely or printed to the console.
42
+
43
+ This is useful when you want full remote logging in production but don't need to send every debug log during local development where you're already watching the console:
44
+
45
+ ```ts
46
+ const logger = createLogger({
47
+ logId: 'your-log-file-id',
48
+ apiKey: 'your-api-key',
49
+ level: process.env.NODE_ENV === 'production' ? 'DEBUG' : 'ERROR',
50
+ });
51
+ ```
52
+
53
+ In this setup, local dev only sends errors remotely (you're already seeing everything in your terminal), while production captures the full picture for dashboard viewing and LLM analysis.
54
+
55
+ ### `silent` option
56
+
57
+ By default, every `logger.*` call also prints to the local console so developers don't lose visibility when replacing `console.log` with `logger.info`. Set `silent: true` in production to disable this.
58
+
59
+ ## Log Methods
60
+
61
+ Every level supports two call styles:
62
+
63
+ ### Simple form — message + optional metadata
64
+
65
+ ```ts
66
+ logger.debug("cache miss", { key: "user:42" });
67
+ logger.info("request handled", { method: "GET", path: "/api/users", ms: 12 });
68
+ logger.warn("rate limit approaching", { current: 95, max: 100 });
69
+ logger.error("query failed", { table: "orders" });
70
+ logger.fatal("database unreachable");
71
+ ```
72
+
73
+ The second argument is a plain metadata object. It can contain nested objects and arrays — the SDK sends it as-is.
74
+
75
+ ### Object form — for per-call group or trace ID
76
+
77
+ ```ts
78
+ logger.info({
79
+ message: "user signed in",
80
+ group: "auth",
81
+ traceId: "req-abc",
82
+ metadata: { userId: 123, method: "oauth" },
83
+ });
84
+ ```
85
+
86
+ Use this when you need to override `group` or `traceId` on a single call without changing the logger's defaults.
87
+
88
+ ## Error Logging
89
+
90
+ `error()` and `fatal()` accept Error objects directly — the SDK extracts the message, stack trace, and error type automatically:
91
+
92
+ ```ts
93
+ try {
94
+ await processPayment(order);
95
+ } catch (err) {
96
+ logger.error(err as Error, { orderId: order.id });
97
+ }
98
+ ```
99
+
100
+ This populates the `stack` and `error_type` columns in ClickHouse so stack traces and exception class names are stored as structured fields, not mashed into the message.
101
+
102
+ ## Scoped Loggers
103
+
104
+ ### Group Logger
105
+
106
+ ```ts
107
+ const authLogger = logger.withGroup("auth");
108
+ authLogger.info("login attempt", { email: "user@example.com" });
109
+ // → group: "auth"
110
+ ```
111
+
112
+ ### Trace Logger
113
+
114
+ ```ts
115
+ const reqLogger = logger.withTraceId("req-abc-123");
116
+ reqLogger.info("handling request");
117
+ reqLogger.warn("slow downstream call", { service: "billing", ms: 800 });
118
+ // → trace_id: "req-abc-123" on both entries
119
+ ```
120
+
121
+ ### Mutating defaults
122
+
123
+ ```ts
124
+ // Set for all subsequent calls
125
+ logger.setGroup("worker");
126
+ logger.setTraceId("job-456");
127
+
128
+ // Clear
129
+ logger.setGroup(undefined);
130
+ logger.setTraceId(undefined);
131
+ ```
132
+
133
+ ## Console Interception
134
+
135
+ Capture existing `console.*` calls without changing application code:
136
+
137
+ ```ts
138
+ logger.interceptConsole(); // uses group "console"
139
+ logger.interceptConsole("app"); // uses custom group
140
+
141
+ // Later, to restore original console behavior:
142
+ logger.restoreConsole();
143
+ ```
144
+
145
+ This wraps `console.debug`, `console.log`, `console.info`, and `console.warn`. Errors are captured via `window.addEventListener('error')` and `unhandledrejection` listeners instead of wrapping `console.error`, to avoid polluting stack traces in frameworks like Next.js/React.
146
+
147
+ ## AI Assistant Integration
148
+
149
+ Run the init command to configure your AI coding assistant (Claude Code, Cursor, etc.) to use the SDK:
150
+
151
+ ```bash
152
+ npx @remote-logger/sdk init
153
+ ```
154
+
155
+ This adds a reference to the SDK's workflow guide in your `CLAUDE.md` and `.cursorrules`. Once configured, your AI assistant will know how to place logs using the SDK and query them via MCP — no copy-pasting logs needed.
156
+
157
+ To set up manually, add this line to your `CLAUDE.md`:
158
+
159
+ ```md
160
+ > See node_modules/@remote-logger/sdk/SKILL.md for the @remote-logger/sdk logging workflow.
161
+ ```
162
+
163
+ ## MCP Server (AI Log Analysis)
164
+
165
+ Remote Logger includes an MCP server that lets AI assistants query your logs directly.
166
+
167
+ ### Add
168
+
169
+ ```bash
170
+ claude mcp add remote-logger https://log.terodato.com/mcp --header "Authorization: Bearer YOUR_API_KEY"
171
+ ```
172
+
173
+ ### Remove
174
+
175
+ ```bash
176
+ claude mcp remove remote-logger
177
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,121 @@
1
+ # @remote-logger/sdk — AI Workflow Guide
2
+
3
+ This project uses Remote Logger to give you persistent runtime insight. You can place log statements freely — they are sent to a central server and you can query them via MCP at any time. This means you don't need to ask the user to copy-paste logs. You place them, the user reproduces the issue, and you query the results yourself.
4
+
5
+ ## The Workflow
6
+
7
+ 1. **Place logs** in the code using the SDK (see API below)
8
+ 2. **Ask the user to reproduce** the issue (or wait for the next occurrence)
9
+ 3. **Query the logs via MCP** using `logs_query` — you have direct access, no copy-pasting needed
10
+ 4. **Analyze and fix** based on the runtime data
11
+ 5. **Repeat** — leave useful logs in place, add more if needed
12
+
13
+ You should default to placing logs early when investigating runtime issues. Don't spend multiple rounds guessing from static code alone — instrument the code, get the data, then fix the bug. Logs persist across runs, so once placed, they keep providing insight.
14
+
15
+ **Cleanup rule:** Use `logger.debug()` for temporary diagnostic logs placed during investigation. Once the issue is resolved, remove all debug-level logs you added. Permanent logs that provide ongoing operational value (errors, warnings, key events) should use the appropriate level and can stay.
16
+
17
+ ## When to Place Logs
18
+
19
+ - Debugging a runtime bug that isn't obvious from code alone
20
+ - Investigating timing issues, race conditions, or intermittent failures
21
+ - Understanding the flow through async code, event handlers, or callbacks
22
+ - Correlating behavior across multiple services or processes (use `traceId`)
23
+ - Monitoring a specific code path after a fix to verify it works
24
+
25
+ ## SDK Quick Reference
26
+
27
+ ### Setup (already done if the project uses this SDK)
28
+
29
+ ```ts
30
+ import { createLogger } from '@remote-logger/sdk';
31
+
32
+ const logger = createLogger({
33
+ logId: 'uuid-from-dashboard', // required — identifies the log stream
34
+ apiKey: 'rl_...', // required — ingestion auth
35
+ group: 'api', // optional: default group for all entries
36
+ traceId: 'req-123', // optional: default trace ID
37
+ level: 'DEBUG', // optional: minimum level (default: 'DEBUG')
38
+ silent: false, // optional: suppress console echo (default: false)
39
+ });
40
+ ```
41
+
42
+ ### Logging
43
+
44
+ ```ts
45
+ logger.debug("cache hit", { key: "user:42" });
46
+ logger.info("request handled", { method: "GET", path: "/api/users", ms: 12 });
47
+ logger.warn("slow query", { duration: 1200 });
48
+ logger.error(err as Error, { orderId: order.id }); // Error objects extract stack + error_type
49
+ logger.fatal(new Error("database unreachable"));
50
+ ```
51
+
52
+ ### Scoped Loggers (for grouping or tracing)
53
+
54
+ ```ts
55
+ const authLogger = logger.withGroup("auth"); // all calls get group: "auth"
56
+ const reqLogger = logger.withTraceId("req-abc"); // all calls get trace_id: "req-abc"
57
+ ```
58
+
59
+ Use `group` to categorize logs by subsystem (e.g., "auth", "billing", "renderer"). Use `traceId` to correlate logs across a single request or operation, especially across services.
60
+
61
+ ### Console Interception
62
+
63
+ ```ts
64
+ logger.interceptConsole(); // captures console.debug/log/info/warn + unhandled errors
65
+ logger.interceptConsole("app"); // with a custom group name
66
+ logger.restoreConsole(); // undo
67
+ ```
68
+
69
+ Use this to capture logs from third-party code or existing console.log statements without modifying them.
70
+
71
+ ## Reading Logs via MCP
72
+
73
+ You have two MCP tools available:
74
+
75
+ - **`logs_list_log_files`** — Lists all log files with data. Call this first to find the log file ID.
76
+ - **`logs_query`** — Execute SQL (ClickHouse) against a log file. Tenant/log filtering is automatic.
77
+
78
+ ### Common Queries
79
+
80
+ ```sql
81
+ -- Recent errors
82
+ SELECT timestamp, level, message, stack FROM logs.entries WHERE level = 'ERROR' ORDER BY timestamp DESC LIMIT 20
83
+
84
+ -- Errors by type
85
+ SELECT error_type, count(*) as cnt FROM logs.entries WHERE error_type IS NOT NULL GROUP BY error_type ORDER BY cnt DESC
86
+
87
+ -- Search for a specific message pattern
88
+ SELECT timestamp, level, group, message FROM logs.entries WHERE message LIKE '%ECONNREFUSED%' ORDER BY timestamp DESC LIMIT 20
89
+
90
+ -- Error rate over time
91
+ SELECT toStartOfMinute(timestamp) as minute, count(*) FROM logs.entries WHERE level = 'ERROR' GROUP BY minute ORDER BY minute
92
+
93
+ -- Logs by group
94
+ SELECT group, count(*) FROM logs.entries GROUP BY group ORDER BY count() DESC
95
+
96
+ -- Trace a specific request across services
97
+ SELECT timestamp, group, level, message FROM logs.entries WHERE trace_id = 'req-abc-123' ORDER BY timestamp
98
+
99
+ -- Recent logs from a specific group
100
+ SELECT timestamp, level, message FROM logs.entries WHERE group = 'auth' ORDER BY timestamp DESC LIMIT 50
101
+ ```
102
+
103
+ ### Table Schema
104
+
105
+ ```
106
+ seq UInt64 -- sequence number
107
+ timestamp DateTime64(3) -- e.g., '2025-01-22 14:30:00.123'
108
+ level String -- DEBUG, INFO, WARN, ERROR, FATAL
109
+ trace_id String -- request correlation
110
+ group String -- categorization (e.g., 'api/billing', 'main', 'renderer')
111
+ message String -- log content
112
+ stack Nullable(String) -- stack trace
113
+ error_type String -- exception class name
114
+ metadata Map(String, String) -- arbitrary key-value pairs
115
+ ```
116
+
117
+ ## Do NOT
118
+
119
+ - Do not pass `batchSize`, `flushInterval`, `httpEndpoint`, `wsEndpoint`, or `transport` — these do not exist.
120
+ - Do not call `errorWithStack()` — it does not exist. Use `logger.error(err)` instead.
121
+ - Do not call `logger.shutdown()` unless explicitly tearing down the logger mid-process. It is not required.
@@ -0,0 +1,130 @@
1
+ type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
2
+ interface LoggerConfig {
3
+ logId: string;
4
+ apiKey?: string;
5
+ group?: string;
6
+ /** Only send logs at this level and above to the remote service (default: 'DEBUG' — sends everything) */
7
+ level?: LogLevel;
8
+ onError?: (error: Error, logs: InternalLogEntry[]) => void;
9
+ traceId?: string;
10
+ /** When true, skip console output from logger.* methods; default false */
11
+ silent?: boolean;
12
+ /** When true, log SDK internal operations to console for troubleshooting */
13
+ debug?: boolean;
14
+ /** @internal Override base URL for development (e.g. 'http://localhost:3500') */
15
+ _endpoint?: string;
16
+ /** @internal Override WebSocket base URL for development (e.g. 'ws://localhost:3501') */
17
+ _wsEndpoint?: string;
18
+ /** @internal Full URL for log ingestion proxy (e.g. '/api/internal/ingest'). When set, uses this as the POST URL directly (no /ingest/http appended), skips WebSocket. */
19
+ _ingestUrl?: string;
20
+ }
21
+ interface InternalLogEntry {
22
+ timestamp: string;
23
+ level: LogLevel;
24
+ message: string;
25
+ logId: string;
26
+ group?: string;
27
+ trace_id?: string;
28
+ stack?: string;
29
+ error_type?: string;
30
+ metadata?: Record<string, unknown>;
31
+ }
32
+ /** Object form for logger.info({ message, group?, metadata? }) */
33
+ interface LogEntryObject {
34
+ message: string;
35
+ group?: string;
36
+ traceId?: string;
37
+ metadata?: Record<string, unknown>;
38
+ }
39
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'failed';
40
+ declare class Logger {
41
+ private config;
42
+ private baseUrl;
43
+ private httpIngestUrl;
44
+ private wsIngestUrl;
45
+ private proxyMode;
46
+ private buffer;
47
+ private flushTimer;
48
+ private flushPromise;
49
+ private consoleIntercepted;
50
+ private httpBackoffMs;
51
+ private maxBackoffMs;
52
+ private consecutiveHttpFailures;
53
+ private _onError;
54
+ private _onUnhandledRejection;
55
+ private ws;
56
+ private wsState;
57
+ private wsReconnectTimer;
58
+ private wsReconnectAttempts;
59
+ private maxWsReconnectAttempts;
60
+ private _onVisibilityChange;
61
+ private _onBeforeUnload;
62
+ constructor(config: LoggerConfig);
63
+ private debugLog;
64
+ private connectWebSocket;
65
+ private startFlushTimer;
66
+ private shouldLog;
67
+ private formatArgs;
68
+ private static readonly CONSOLE_METHOD;
69
+ private log;
70
+ private parseArgs;
71
+ debug(entry: LogEntryObject): void;
72
+ debug(message: string, metadata?: Record<string, unknown>): void;
73
+ info(entry: LogEntryObject): void;
74
+ info(message: string, metadata?: Record<string, unknown>): void;
75
+ warn(entry: LogEntryObject): void;
76
+ warn(message: string, metadata?: Record<string, unknown>): void;
77
+ error(entry: LogEntryObject): void;
78
+ error(message: string, metadata?: Record<string, unknown>): void;
79
+ error(err: Error, metadata?: Record<string, unknown>): void;
80
+ fatal(entry: LogEntryObject): void;
81
+ fatal(message: string, metadata?: Record<string, unknown>): void;
82
+ fatal(err: Error, metadata?: Record<string, unknown>): void;
83
+ withTraceId(traceId: string): TraceLogger;
84
+ withGroup(group: string): GroupLogger;
85
+ setTraceId(traceId: string | undefined): void;
86
+ setGroup(group: string | undefined): void;
87
+ interceptConsole(group?: string): void;
88
+ restoreConsole(): void;
89
+ flush(): Promise<void>;
90
+ private doFlush;
91
+ private sendViaWebSocket;
92
+ private sendViaHttp;
93
+ /** Fire-and-forget flush using fetch with keepalive (works during page unload) */
94
+ private flushSync;
95
+ private registerLifecycleHooks;
96
+ /** Check server connectivity and API key validity */
97
+ ping(): Promise<{
98
+ ok: boolean;
99
+ latencyMs: number;
100
+ error?: string;
101
+ }>;
102
+ /** Get current connection status */
103
+ getConnectionStatus(): {
104
+ transport: 'websocket' | 'http';
105
+ state: ConnectionState;
106
+ };
107
+ }
108
+ declare class TraceLogger {
109
+ private parent;
110
+ private traceId;
111
+ constructor(parent: Logger, traceId: string);
112
+ debug(message: string, metadata?: Record<string, unknown>): void;
113
+ info(message: string, metadata?: Record<string, unknown>): void;
114
+ warn(message: string, metadata?: Record<string, unknown>): void;
115
+ error(message: string, metadata?: Record<string, unknown>): void;
116
+ fatal(message: string, metadata?: Record<string, unknown>): void;
117
+ }
118
+ declare class GroupLogger {
119
+ private parent;
120
+ private group;
121
+ constructor(parent: Logger, group: string);
122
+ debug(message: string, metadata?: Record<string, unknown>): void;
123
+ info(message: string, metadata?: Record<string, unknown>): void;
124
+ warn(message: string, metadata?: Record<string, unknown>): void;
125
+ error(message: string, metadata?: Record<string, unknown>): void;
126
+ fatal(message: string, metadata?: Record<string, unknown>): void;
127
+ }
128
+ declare function createLogger(config: LoggerConfig): Logger;
129
+
130
+ export { type InternalLogEntry, type LogEntryObject, type LogLevel, Logger, type LoggerConfig, createLogger, createLogger as default };
@@ -0,0 +1,130 @@
1
+ type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
2
+ interface LoggerConfig {
3
+ logId: string;
4
+ apiKey?: string;
5
+ group?: string;
6
+ /** Only send logs at this level and above to the remote service (default: 'DEBUG' — sends everything) */
7
+ level?: LogLevel;
8
+ onError?: (error: Error, logs: InternalLogEntry[]) => void;
9
+ traceId?: string;
10
+ /** When true, skip console output from logger.* methods; default false */
11
+ silent?: boolean;
12
+ /** When true, log SDK internal operations to console for troubleshooting */
13
+ debug?: boolean;
14
+ /** @internal Override base URL for development (e.g. 'http://localhost:3500') */
15
+ _endpoint?: string;
16
+ /** @internal Override WebSocket base URL for development (e.g. 'ws://localhost:3501') */
17
+ _wsEndpoint?: string;
18
+ /** @internal Full URL for log ingestion proxy (e.g. '/api/internal/ingest'). When set, uses this as the POST URL directly (no /ingest/http appended), skips WebSocket. */
19
+ _ingestUrl?: string;
20
+ }
21
+ interface InternalLogEntry {
22
+ timestamp: string;
23
+ level: LogLevel;
24
+ message: string;
25
+ logId: string;
26
+ group?: string;
27
+ trace_id?: string;
28
+ stack?: string;
29
+ error_type?: string;
30
+ metadata?: Record<string, unknown>;
31
+ }
32
+ /** Object form for logger.info({ message, group?, metadata? }) */
33
+ interface LogEntryObject {
34
+ message: string;
35
+ group?: string;
36
+ traceId?: string;
37
+ metadata?: Record<string, unknown>;
38
+ }
39
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'failed';
40
+ declare class Logger {
41
+ private config;
42
+ private baseUrl;
43
+ private httpIngestUrl;
44
+ private wsIngestUrl;
45
+ private proxyMode;
46
+ private buffer;
47
+ private flushTimer;
48
+ private flushPromise;
49
+ private consoleIntercepted;
50
+ private httpBackoffMs;
51
+ private maxBackoffMs;
52
+ private consecutiveHttpFailures;
53
+ private _onError;
54
+ private _onUnhandledRejection;
55
+ private ws;
56
+ private wsState;
57
+ private wsReconnectTimer;
58
+ private wsReconnectAttempts;
59
+ private maxWsReconnectAttempts;
60
+ private _onVisibilityChange;
61
+ private _onBeforeUnload;
62
+ constructor(config: LoggerConfig);
63
+ private debugLog;
64
+ private connectWebSocket;
65
+ private startFlushTimer;
66
+ private shouldLog;
67
+ private formatArgs;
68
+ private static readonly CONSOLE_METHOD;
69
+ private log;
70
+ private parseArgs;
71
+ debug(entry: LogEntryObject): void;
72
+ debug(message: string, metadata?: Record<string, unknown>): void;
73
+ info(entry: LogEntryObject): void;
74
+ info(message: string, metadata?: Record<string, unknown>): void;
75
+ warn(entry: LogEntryObject): void;
76
+ warn(message: string, metadata?: Record<string, unknown>): void;
77
+ error(entry: LogEntryObject): void;
78
+ error(message: string, metadata?: Record<string, unknown>): void;
79
+ error(err: Error, metadata?: Record<string, unknown>): void;
80
+ fatal(entry: LogEntryObject): void;
81
+ fatal(message: string, metadata?: Record<string, unknown>): void;
82
+ fatal(err: Error, metadata?: Record<string, unknown>): void;
83
+ withTraceId(traceId: string): TraceLogger;
84
+ withGroup(group: string): GroupLogger;
85
+ setTraceId(traceId: string | undefined): void;
86
+ setGroup(group: string | undefined): void;
87
+ interceptConsole(group?: string): void;
88
+ restoreConsole(): void;
89
+ flush(): Promise<void>;
90
+ private doFlush;
91
+ private sendViaWebSocket;
92
+ private sendViaHttp;
93
+ /** Fire-and-forget flush using fetch with keepalive (works during page unload) */
94
+ private flushSync;
95
+ private registerLifecycleHooks;
96
+ /** Check server connectivity and API key validity */
97
+ ping(): Promise<{
98
+ ok: boolean;
99
+ latencyMs: number;
100
+ error?: string;
101
+ }>;
102
+ /** Get current connection status */
103
+ getConnectionStatus(): {
104
+ transport: 'websocket' | 'http';
105
+ state: ConnectionState;
106
+ };
107
+ }
108
+ declare class TraceLogger {
109
+ private parent;
110
+ private traceId;
111
+ constructor(parent: Logger, traceId: string);
112
+ debug(message: string, metadata?: Record<string, unknown>): void;
113
+ info(message: string, metadata?: Record<string, unknown>): void;
114
+ warn(message: string, metadata?: Record<string, unknown>): void;
115
+ error(message: string, metadata?: Record<string, unknown>): void;
116
+ fatal(message: string, metadata?: Record<string, unknown>): void;
117
+ }
118
+ declare class GroupLogger {
119
+ private parent;
120
+ private group;
121
+ constructor(parent: Logger, group: string);
122
+ debug(message: string, metadata?: Record<string, unknown>): void;
123
+ info(message: string, metadata?: Record<string, unknown>): void;
124
+ warn(message: string, metadata?: Record<string, unknown>): void;
125
+ error(message: string, metadata?: Record<string, unknown>): void;
126
+ fatal(message: string, metadata?: Record<string, unknown>): void;
127
+ }
128
+ declare function createLogger(config: LoggerConfig): Logger;
129
+
130
+ export { type InternalLogEntry, type LogEntryObject, type LogLevel, Logger, type LoggerConfig, createLogger, createLogger as default };