@donkeylabs/server 2.0.5 → 2.0.7

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 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 = log.child({ orderId });
136
+ // Create request-specific logger with context
137
+ const requestLog = ctx.core.logger.child({ orderId });
98
138
 
99
139
  requestLog.info("Processing payment");
100
- // Logs: { plugin: "payments", orderId: "123", ... }
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
- [12:34:56.789] INFO User logged in {"userId":123}
153
- [12:34:56.790] ERROR Payment failed {"orderId":456,"error":"Insufficient funds"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -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
- const LEVEL_COLORS: Record<LogLevel, string> = {
40
- debug: "\x1b[90m", // gray
41
- info: "\x1b[36m", // cyan
42
- warn: "\x1b[33m", // yellow
43
- error: "\x1b[31m", // red
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
- const RESET = "\x1b[0m";
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 color = LEVEL_COLORS[entry.level];
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 = `${color}[${time}] ${level}${RESET} ${entry.message}`;
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 += ` ${"\x1b[90m"}${JSON.stringify(extra)}${RESET}`;
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(config: LoggerConfig = {}, context: Record<string, any> = {}) {
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: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k as LogLevel] === this.minLevel) as LogLevel, transports: this.transports },
124
- { ...this.context, ...context }
185
+ { level: levelKey, transports: this.transports },
186
+ { ...this.context },
187
+ [...this.tags, name]
125
188
  );
126
189
  }
127
190
  }
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
- public readonly core: CoreServices,
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.core.db as unknown as Kysely<Schema>;
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) {
package/src/server.ts CHANGED
@@ -1278,6 +1278,7 @@ ${factoryFunction}
1278
1278
  Bun.serve({
1279
1279
  port: currentPort,
1280
1280
  fetch: fetchHandler,
1281
+ idleTimeout: 255, // Max value (255 seconds) for SSE/long-lived connections
1281
1282
  });
1282
1283
  // Update the actual port we're running on
1283
1284
  this.port = currentPort;