@donkeylabs/server 2.0.4 → 2.0.6
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/docs/logger.md +57 -12
- package/package.json +1 -1
- package/src/core/logger.ts +79 -16
- package/src/core/sse.ts +4 -0
- package/src/core.ts +25 -5
package/docs/logger.md
CHANGED
|
@@ -23,6 +23,7 @@ interface Logger {
|
|
|
23
23
|
warn(message: string, data?: Record<string, any>): void;
|
|
24
24
|
error(message: string, data?: Record<string, any>): void;
|
|
25
25
|
child(context: Record<string, any>): Logger;
|
|
26
|
+
tag(name: string): Logger; // Create tagged child logger
|
|
26
27
|
}
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -81,23 +82,62 @@ router.route("checkout").typed({
|
|
|
81
82
|
});
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
### Tagged Loggers
|
|
86
|
+
|
|
87
|
+
Tags add colored prefixes to log messages for visual organization. Each tag gets a consistent color.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Create tagged logger
|
|
91
|
+
const dbLog = ctx.core.logger.tag("database");
|
|
92
|
+
dbLog.info("Query executed");
|
|
93
|
+
// Output: 12:34:56.789 INFO [database] Query executed
|
|
94
|
+
|
|
95
|
+
// Chain multiple tags
|
|
96
|
+
const queryLog = dbLog.tag("slow-query");
|
|
97
|
+
queryLog.warn("Query took 5s", { table: "orders" });
|
|
98
|
+
// Output: 12:34:56.790 WARN [database] [slow-query] Query took 5s {"table":"orders"}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Plugin Auto-Tagging:** Plugins automatically get a tagged logger with the plugin name:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// In plugin service - ctx.core.logger is auto-tagged with plugin name
|
|
105
|
+
export const ordersPlugin = createPlugin.define({
|
|
106
|
+
name: "orders",
|
|
107
|
+
service: async (ctx) => {
|
|
108
|
+
// Logger is already tagged with [orders]
|
|
109
|
+
ctx.core.logger.info("Plugin initialized");
|
|
110
|
+
// Output: 12:34:56.789 INFO [orders] Plugin initialized
|
|
111
|
+
|
|
112
|
+
// Add additional tags as needed
|
|
113
|
+
const paymentLog = ctx.core.logger.tag("payments");
|
|
114
|
+
paymentLog.info("Processing");
|
|
115
|
+
// Output: 12:34:56.790 INFO [orders] [payments] Processing
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
create: async (data) => {
|
|
119
|
+
ctx.core.logger.info("Creating order", { total: data.total });
|
|
120
|
+
// Output: 12:34:56.791 INFO [orders] Creating order {"total":100}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
84
127
|
### Child Loggers
|
|
85
128
|
|
|
86
|
-
Child loggers inherit parent settings and add persistent context:
|
|
129
|
+
Child loggers inherit parent settings and add persistent context data (not visible as tags):
|
|
87
130
|
|
|
88
131
|
```ts
|
|
89
132
|
// In plugin initialization
|
|
90
133
|
service: async (ctx) => {
|
|
91
|
-
// Create logger with plugin context
|
|
92
|
-
const log = ctx.core.logger.child({ plugin: "payments" });
|
|
93
|
-
|
|
94
134
|
return {
|
|
95
135
|
async processPayment(orderId: string) {
|
|
96
|
-
// Create request-specific logger
|
|
97
|
-
const requestLog =
|
|
136
|
+
// Create request-specific logger with context
|
|
137
|
+
const requestLog = ctx.core.logger.child({ orderId });
|
|
98
138
|
|
|
99
139
|
requestLog.info("Processing payment");
|
|
100
|
-
//
|
|
140
|
+
// Output: 12:34:56.789 INFO [payments] Processing payment {"orderId":"123"}
|
|
101
141
|
|
|
102
142
|
requestLog.debug("Validating card");
|
|
103
143
|
requestLog.info("Payment complete");
|
|
@@ -146,20 +186,24 @@ const requestLogger = createMiddleware(async (req, ctx, next) => {
|
|
|
146
186
|
|
|
147
187
|
### Pretty Format (Default)
|
|
148
188
|
|
|
149
|
-
Human-readable colored output for development:
|
|
189
|
+
Human-readable colored output for development. Tags appear as colored `[tag]` prefixes:
|
|
150
190
|
|
|
151
191
|
```
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
12:34:56.789 INFO User logged in {"userId":123}
|
|
193
|
+
12:34:56.790 ERROR [orders] Payment failed {"orderId":456,"error":"Insufficient funds"}
|
|
194
|
+
12:34:56.791 WARN [orders] [payments] Retry attempt {"attempt":3}
|
|
154
195
|
```
|
|
155
196
|
|
|
197
|
+
Each tag gets a consistent color (cyan, magenta, green, yellow, blue, red) that persists across the application lifetime.
|
|
198
|
+
|
|
156
199
|
### JSON Format
|
|
157
200
|
|
|
158
|
-
Structured JSON for production log aggregation:
|
|
201
|
+
Structured JSON for production log aggregation. Tags are included as an array:
|
|
159
202
|
|
|
160
203
|
```json
|
|
161
204
|
{"timestamp":"2024-01-15T12:34:56.789Z","level":"info","message":"User logged in","userId":123}
|
|
162
|
-
{"timestamp":"2024-01-15T12:34:56.790Z","level":"error","message":"Payment failed","orderId":456,"error":"Insufficient funds"}
|
|
205
|
+
{"timestamp":"2024-01-15T12:34:56.790Z","level":"error","message":"Payment failed","tags":["orders"],"orderId":456,"error":"Insufficient funds"}
|
|
206
|
+
{"timestamp":"2024-01-15T12:34:56.791Z","level":"warn","message":"Retry attempt","tags":["orders","payments"],"attempt":3}
|
|
163
207
|
```
|
|
164
208
|
|
|
165
209
|
---
|
|
@@ -296,6 +340,7 @@ interface LogEntry {
|
|
|
296
340
|
timestamp: Date;
|
|
297
341
|
level: "debug" | "info" | "warn" | "error";
|
|
298
342
|
message: string;
|
|
343
|
+
tags?: string[]; // From tag() - displayed as colored prefixes
|
|
299
344
|
data?: Record<string, any>; // Per-call data
|
|
300
345
|
context?: Record<string, any>; // From child logger
|
|
301
346
|
}
|
package/package.json
CHANGED
package/src/core/logger.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Core Logger Service
|
|
2
|
-
// Structured logging with levels and child loggers
|
|
2
|
+
// Structured logging with levels, tags, and child loggers
|
|
3
|
+
|
|
4
|
+
import pc from "picocolors";
|
|
3
5
|
|
|
4
6
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
5
7
|
|
|
@@ -7,6 +9,7 @@ export interface LogEntry {
|
|
|
7
9
|
timestamp: Date;
|
|
8
10
|
level: LogLevel;
|
|
9
11
|
message: string;
|
|
12
|
+
tags?: string[];
|
|
10
13
|
data?: Record<string, any>;
|
|
11
14
|
context?: Record<string, any>;
|
|
12
15
|
}
|
|
@@ -27,6 +30,8 @@ export interface Logger {
|
|
|
27
30
|
warn(message: string, data?: Record<string, any>): void;
|
|
28
31
|
error(message: string, data?: Record<string, any>): void;
|
|
29
32
|
child(context: Record<string, any>): Logger;
|
|
33
|
+
/** Create a tagged child logger with colored prefix */
|
|
34
|
+
tag(name: string): Logger;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
@@ -36,14 +41,39 @@ const LOG_LEVELS: Record<LogLevel, number> = {
|
|
|
36
41
|
error: 3,
|
|
37
42
|
};
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// Tag color palette - cycles through these for auto-assigned colors
|
|
45
|
+
const TAG_COLORS: ((s: string) => string)[] = [
|
|
46
|
+
pc.cyan,
|
|
47
|
+
pc.magenta,
|
|
48
|
+
pc.green,
|
|
49
|
+
pc.yellow,
|
|
50
|
+
pc.blue,
|
|
51
|
+
pc.red,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Cache for consistent tag colors across the app
|
|
55
|
+
const tagColorCache = new Map<string, (s: string) => string>();
|
|
56
|
+
let colorIndex = 0;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a consistent color for a tag name.
|
|
60
|
+
* Same tag always gets the same color within a process.
|
|
61
|
+
*/
|
|
62
|
+
function getTagColor(tag: string): (s: string) => string {
|
|
63
|
+
if (!tagColorCache.has(tag)) {
|
|
64
|
+
tagColorCache.set(tag, TAG_COLORS[colorIndex % TAG_COLORS.length]!);
|
|
65
|
+
colorIndex++;
|
|
66
|
+
}
|
|
67
|
+
return tagColorCache.get(tag)!;
|
|
68
|
+
}
|
|
45
69
|
|
|
46
|
-
|
|
70
|
+
// Level colors using picocolors
|
|
71
|
+
const LEVEL_COLORS: Record<LogLevel, (s: string) => string> = {
|
|
72
|
+
debug: pc.gray,
|
|
73
|
+
info: pc.blue,
|
|
74
|
+
warn: pc.yellow,
|
|
75
|
+
error: pc.red,
|
|
76
|
+
};
|
|
47
77
|
|
|
48
78
|
// Console transport with pretty or JSON formatting
|
|
49
79
|
export class ConsoleTransport implements LogTransport {
|
|
@@ -55,19 +85,30 @@ export class ConsoleTransport implements LogTransport {
|
|
|
55
85
|
timestamp: entry.timestamp.toISOString(),
|
|
56
86
|
level: entry.level,
|
|
57
87
|
message: entry.message,
|
|
88
|
+
tags: entry.tags,
|
|
58
89
|
...entry.data,
|
|
59
90
|
...entry.context,
|
|
60
91
|
}));
|
|
61
92
|
} else {
|
|
62
|
-
const
|
|
63
|
-
const time = entry.timestamp.toISOString().slice(11, 23);
|
|
64
|
-
const level = entry.level.toUpperCase().padEnd(5);
|
|
93
|
+
const levelColor = LEVEL_COLORS[entry.level];
|
|
94
|
+
const time = pc.dim(entry.timestamp.toISOString().slice(11, 23));
|
|
95
|
+
const level = levelColor(entry.level.toUpperCase().padEnd(5));
|
|
96
|
+
|
|
97
|
+
// Build tag prefix with colors
|
|
98
|
+
let tagPrefix = "";
|
|
99
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
100
|
+
const tagParts = entry.tags.map(tag => {
|
|
101
|
+
const colorFn = getTagColor(tag);
|
|
102
|
+
return colorFn(`[${tag}]`);
|
|
103
|
+
});
|
|
104
|
+
tagPrefix = tagParts.join(" ") + " ";
|
|
105
|
+
}
|
|
65
106
|
|
|
66
|
-
let output = `${
|
|
107
|
+
let output = `${time} ${level} ${tagPrefix}${entry.message}`;
|
|
67
108
|
|
|
68
109
|
const extra = { ...entry.data, ...entry.context };
|
|
69
110
|
if (Object.keys(extra).length > 0) {
|
|
70
|
-
output += ` ${
|
|
111
|
+
output += ` ${pc.dim(JSON.stringify(extra))}`;
|
|
71
112
|
}
|
|
72
113
|
|
|
73
114
|
console.log(output);
|
|
@@ -79,11 +120,17 @@ class LoggerImpl implements Logger {
|
|
|
79
120
|
private minLevel: number;
|
|
80
121
|
private transports: LogTransport[];
|
|
81
122
|
private context: Record<string, any>;
|
|
123
|
+
private tags: string[];
|
|
82
124
|
|
|
83
|
-
constructor(
|
|
125
|
+
constructor(
|
|
126
|
+
config: LoggerConfig = {},
|
|
127
|
+
context: Record<string, any> = {},
|
|
128
|
+
tags: string[] = []
|
|
129
|
+
) {
|
|
84
130
|
this.minLevel = LOG_LEVELS[config.level ?? "info"];
|
|
85
131
|
this.transports = config.transports ?? [new ConsoleTransport(config.format ?? "pretty")];
|
|
86
132
|
this.context = context;
|
|
133
|
+
this.tags = tags;
|
|
87
134
|
}
|
|
88
135
|
|
|
89
136
|
private log(level: LogLevel, message: string, data?: Record<string, any>): void {
|
|
@@ -93,6 +140,7 @@ class LoggerImpl implements Logger {
|
|
|
93
140
|
timestamp: new Date(),
|
|
94
141
|
level,
|
|
95
142
|
message,
|
|
143
|
+
tags: this.tags.length > 0 ? this.tags : undefined,
|
|
96
144
|
data,
|
|
97
145
|
context: Object.keys(this.context).length > 0 ? this.context : undefined,
|
|
98
146
|
};
|
|
@@ -119,9 +167,24 @@ class LoggerImpl implements Logger {
|
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
child(context: Record<string, any>): Logger {
|
|
170
|
+
const levelKey = Object.keys(LOG_LEVELS).find(
|
|
171
|
+
k => LOG_LEVELS[k as LogLevel] === this.minLevel
|
|
172
|
+
) as LogLevel;
|
|
173
|
+
return new LoggerImpl(
|
|
174
|
+
{ level: levelKey, transports: this.transports },
|
|
175
|
+
{ ...this.context, ...context },
|
|
176
|
+
[...this.tags]
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
tag(name: string): Logger {
|
|
181
|
+
const levelKey = Object.keys(LOG_LEVELS).find(
|
|
182
|
+
k => LOG_LEVELS[k as LogLevel] === this.minLevel
|
|
183
|
+
) as LogLevel;
|
|
122
184
|
return new LoggerImpl(
|
|
123
|
-
{ level:
|
|
124
|
-
{ ...this.context
|
|
185
|
+
{ level: levelKey, transports: this.transports },
|
|
186
|
+
{ ...this.context },
|
|
187
|
+
[...this.tags, name]
|
|
125
188
|
);
|
|
126
189
|
}
|
|
127
190
|
}
|
package/src/core/sse.ts
CHANGED
|
@@ -58,6 +58,10 @@ class SSEImpl implements SSE {
|
|
|
58
58
|
// Send retry interval to client
|
|
59
59
|
const retryMsg = `retry: ${this.retryInterval}\n\n`;
|
|
60
60
|
controller.enqueue(this.encoder.encode(retryMsg));
|
|
61
|
+
|
|
62
|
+
// Send immediate heartbeat to establish connection and prevent early timeout
|
|
63
|
+
const heartbeat = `: heartbeat ${Date.now()}\n\n`;
|
|
64
|
+
controller.enqueue(this.encoder.encode(heartbeat));
|
|
61
65
|
},
|
|
62
66
|
cancel: () => {
|
|
63
67
|
this.removeClient(id);
|
package/src/core.ts
CHANGED
|
@@ -157,14 +157,34 @@ export interface GlobalContext {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
export class PluginContext<Deps = any, Schema = any, Config = void> {
|
|
160
|
+
private _core: CoreServices;
|
|
161
|
+
private _taggedCore?: CoreServices;
|
|
162
|
+
private _pluginName?: string;
|
|
163
|
+
|
|
160
164
|
constructor(
|
|
161
|
-
|
|
165
|
+
core: CoreServices,
|
|
162
166
|
public readonly deps: Deps,
|
|
163
|
-
public readonly config: Config
|
|
164
|
-
|
|
167
|
+
public readonly config: Config,
|
|
168
|
+
pluginName?: string
|
|
169
|
+
) {
|
|
170
|
+
this._core = core;
|
|
171
|
+
this._pluginName = pluginName;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Core services with auto-tagged logger for the plugin */
|
|
175
|
+
get core(): CoreServices {
|
|
176
|
+
// Lazily create tagged core services
|
|
177
|
+
if (!this._taggedCore && this._pluginName) {
|
|
178
|
+
this._taggedCore = {
|
|
179
|
+
...this._core,
|
|
180
|
+
logger: this._core.logger.tag(this._pluginName),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return this._taggedCore || this._core;
|
|
184
|
+
}
|
|
165
185
|
|
|
166
186
|
get db(): Kysely<Schema> {
|
|
167
|
-
return this.
|
|
187
|
+
return this._core.db as unknown as Kysely<Schema>;
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -676,7 +696,7 @@ export class PluginManager {
|
|
|
676
696
|
}
|
|
677
697
|
|
|
678
698
|
const pluginConfig = (plugin as ConfiguredPlugin)._boundConfig;
|
|
679
|
-
const ctx = new PluginContext(this.core, pluginDeps, pluginConfig);
|
|
699
|
+
const ctx = new PluginContext(this.core, pluginDeps, pluginConfig, plugin.name);
|
|
680
700
|
const service = await plugin.service(ctx);
|
|
681
701
|
|
|
682
702
|
if (service) {
|