@spfn/monitor 0.1.0-beta.2 → 0.1.0-beta.21

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 INFLIKE Inc.
3
+ Copyright (c) 2025 FXY Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,28 +1,54 @@
1
- # @spfn/monitor
1
+ # @spfn/monitor — DB-backed error tracking, logging, and admin dashboard
2
2
 
3
- Error tracking, log management, and monitoring dashboard for SPFN applications.
3
+ DB-persisted error tracking with fingerprint deduplication and state-based Slack
4
+ notifications, a pluggable developer-log store, superadmin admin routes, and ready-made
5
+ React dashboard components. Integrates into an SPFN server via `defineServerConfig()`
6
+ (`onError` middleware + lifecycle) and a package router mounted with `.packages([...])`.
4
7
 
5
- ## Features
6
-
7
- - **DB-backed error tracking**: Fingerprint-based deduplication with automatic grouping
8
- - **State-based notifications**: Slack alerts only on new errors and reopened errors (no duplicates)
9
- - **Developer logging**: Pluggable log storage with DB default
10
- - **Admin API**: Superadmin-only routes for managing errors and viewing logs
11
- - **React dashboard**: Ready-to-use monitoring UI components
12
-
13
- ## Installation
8
+ ## Install
14
9
 
15
10
  ```bash
16
11
  pnpm add @spfn/monitor
17
12
  ```
18
13
 
19
- ## Quick Start
14
+ Peer dep: `next` (`^15 || ^16`, optional — only needed for the `nextjs/client` components).
15
+ Workspace deps `@spfn/core`, `@spfn/auth`, `@spfn/notification` come transitively.
16
+
17
+ ## Import paths
20
18
 
21
- ### Server Configuration
19
+ There are **four** entry points (from `package.json` `exports`):
22
20
 
23
21
  ```typescript
22
+ // Client-safe: API client + shared types/consts
23
+ import { monitorApi, type MonitorRouter, type MonitorStats } from '@spfn/monitor';
24
+
25
+ // Server-only: router, integrations, services, repos, entities (uses node:crypto, DB)
26
+ import { monitorRouter, createMonitorErrorHandler, writeLog } from '@spfn/monitor/server';
27
+
28
+ // Config: code-level overrides + env getters + schema
29
+ import { configureMonitor, getMonitorConfig } from '@spfn/monitor/config';
30
+
31
+ // React components ('use client') — Next.js only
32
+ import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
33
+ ```
34
+
35
+ Importing `@spfn/monitor/server` runs `@spfn/monitor/config` as a side effect (env registry
36
+ is built). Never import `/server` or `/config`'s `env`-touching code into client/edge bundles.
37
+
38
+ ---
39
+
40
+ ## Setup (SPFN server)
41
+
42
+ Three integration points, all on `defineServerConfig()` + the app router:
43
+
44
+ ```typescript
45
+ // src/server/server.config.ts
24
46
  import { defineServerConfig } from '@spfn/core/server';
25
- import { createMonitorErrorHandler, createMonitorLifecycle, monitorRouter } from '@spfn/monitor/server';
47
+ import {
48
+ createMonitorErrorHandler,
49
+ createMonitorLifecycle,
50
+ } from '@spfn/monitor/server';
51
+ import { appRouter } from './router';
26
52
 
27
53
  export default defineServerConfig()
28
54
  .middleware({ onError: createMonitorErrorHandler() })
@@ -31,202 +57,398 @@ export default defineServerConfig()
31
57
  .build();
32
58
  ```
33
59
 
34
- Register the monitor router as a package router:
60
+ Mount `monitorRouter` as a **package router** on your app router (so its `/_monitor/admin/*`
61
+ routes are served, but stay out of `AppRouter`'s client types — call them via `monitorApi`):
35
62
 
36
63
  ```typescript
64
+ // src/server/router.ts
65
+ import { defineRouter } from '@spfn/core/route';
37
66
  import { monitorRouter } from '@spfn/monitor/server';
67
+ import { authRouter } from '@spfn/auth/server';
38
68
 
39
- export const appRouter = defineRouter({ ... })
69
+ export const appRouter = defineRouter({ /* your routes */ })
40
70
  .packages([authRouter, monitorRouter]);
41
71
  ```
42
72
 
43
- ### Database Migration
73
+ > `monitorRouter` is built with `defineRouter({...})` — there is no `mergeRouters` export in
74
+ > `@spfn/core/route`. Use `.packages([monitorRouter])`, not `mergeRouters(...)`.
75
+
76
+ ### Database migration
77
+
78
+ `monitorSchema = createSchema('@spfn/monitor')` → the PostgreSQL schema **`spfn_monitor`**
79
+ with tables `error_groups`, `error_events`, `logs`.
44
80
 
45
81
  ```bash
46
- spfn db migrate
82
+ pnpm spfn db push # dev: push schema directly
83
+ # or, with migration history (production):
84
+ pnpm spfn db generate && pnpm spfn db migrate
47
85
  ```
48
86
 
49
- This creates the `spfn_monitor` schema with `error_groups`, `error_events`, and `logs` tables.
87
+ `@spfn/monitor` ships pre-generated SQL under `migrations/` (declared via `package.json`
88
+ `spfn.migrations.dir`); SPFN's CLI picks up the package's entities (`spfn.schemas`).
89
+
90
+ ---
50
91
 
51
92
  ## Configuration
52
93
 
53
- ### Environment Variables
94
+ ### Environment variables
54
95
 
55
- ```bash
56
- # Slack webhook for error notifications (optional)
57
- SPFN_MONITOR_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
96
+ All optional. Resolution order is **code config > env > built-in default**.
58
97
 
59
- # Error retention in days (default: 90)
60
- SPFN_MONITOR_ERROR_RETENTION_DAYS=90
98
+ | Variable | Default | Description |
99
+ |----------|---------|-------------|
100
+ | `SPFN_MONITOR_SLACK_WEBHOOK_URL` | — | Slack webhook for error notifications (falls back to `@spfn/notification` default) |
101
+ | `SPFN_MONITOR_ERROR_RETENTION_DAYS` | `90` | Days to retain error events |
102
+ | `SPFN_MONITOR_LOG_RETENTION_DAYS` | `30` | Days to retain logs |
103
+ | `SPFN_MONITOR_MIN_STATUS_CODE` | `500` | Minimum HTTP status code tracked as an error |
61
104
 
62
- # Log retention in days (default: 30)
63
- SPFN_MONITOR_LOG_RETENTION_DAYS=30
105
+ The webhook URL is a secret — keep it in `.env.server` (server-only), never a committed file.
64
106
 
65
- # Minimum HTTP status code to track (default: 500)
66
- SPFN_MONITOR_MIN_STATUS_CODE=500
67
- ```
68
-
69
- ### Code Configuration
107
+ ### Code config
70
108
 
71
109
  ```typescript
72
110
  import { configureMonitor } from '@spfn/monitor/config';
73
111
 
74
112
  configureMonitor({
75
- slackWebhookUrl: 'https://hooks.slack.com/services/...',
113
+ slackWebhookUrl: process.env.SPFN_MONITOR_SLACK_WEBHOOK_URL, // override
76
114
  errorRetentionDays: 90,
77
115
  logRetentionDays: 30,
78
116
  minStatusCode: 500,
79
117
  });
80
118
  ```
81
119
 
82
- ## Error Tracking
120
+ `MonitorConfig` keys: `slackWebhookUrl?`, `errorRetentionDays?`, `logRetentionDays?`,
121
+ `minStatusCode?`. Getters resolve config > env > default:
122
+ `getMonitorConfig()`, `getSlackWebhookUrl()`, `getErrorRetentionDays()`,
123
+ `getLogRetentionDays()`, `getMinStatusCode()`. The validated env proxy is exported as `env`,
124
+ the schema as `monitorEnvSchema`.
125
+
126
+ > Retention values are *exposed* config, but this package does **not** ship a purge
127
+ > scheduler. `LogStore.purge(olderThan)` and `logsRepository.deleteOlderThan(date)` exist;
128
+ > wire up your own cron/job if you want automatic retention enforcement.
83
129
 
84
- Errors are automatically tracked when using `createMonitorErrorHandler()`:
130
+ ---
85
131
 
86
- 1. **New error** - Creates error group + event, sends Slack notification
87
- 2. **Repeated error** (active/ignored) - Increments count, records event, no notification
88
- 3. **Reopened error** (was resolved) - Changes status to active, sends Slack notification
132
+ ## Error tracking
133
+
134
+ `createMonitorErrorHandler(options?)` returns an `onError` callback (signature
135
+ `(err: Error, ctx) => Promise<void>`) — a drop-in replacement for
136
+ `createErrorSlackNotifier` from `@spfn/notification`. On each error at/above `minStatusCode`
137
+ it calls `trackError`, which does state-based deduplication:
138
+
139
+ | Existing group state | Action | Slack? |
140
+ |----------------------|--------|--------|
141
+ | none (new fingerprint) | create group + event | yes (`new`) |
142
+ | `active` / `ignored` | increment count + create event | no |
143
+ | `resolved` | reopen → `active`, increment, create event | yes (`reopened`) |
144
+
145
+ Errors below `minStatusCode` are skipped. Slack/event failures are caught and logged — they
146
+ never break the response.
147
+
148
+ ```typescript
149
+ createMonitorErrorHandler({
150
+ minStatusCode: 500, // default: getMinStatusCode() (env or 500)
151
+ environment: process.env.NODE_ENV, // shown in the Slack title, e.g. "[production]"
152
+ extractMetadata: (err, ctx) => ({ // stored on the error_event row
153
+ serverInstance: process.env.HOSTNAME,
154
+ }),
155
+ });
156
+ ```
157
+
158
+ `MonitorErrorHandlerOptions`: `minStatusCode?`, `environment?`, `extractMetadata?(err, ctx)`.
89
159
 
90
160
  ### Fingerprinting
91
161
 
92
- Errors are grouped by a SHA-256 fingerprint of `name:message:path`, producing a 16-character hex ID.
162
+ `generateFingerprint(name, message, path)` `SHA-256(name:message:path)` sliced to the
163
+ first **16 hex chars**. Same name+message+path ⇒ same group. The `path` component means the
164
+ same error from different routes is grouped separately.
93
165
 
94
- ### Manual Error Tracking
166
+ ### Manual tracking
95
167
 
96
168
  ```typescript
97
169
  import { trackError } from '@spfn/monitor/server';
98
170
 
99
- await trackError(error, {
100
- statusCode: 500,
101
- path: '/api/example',
102
- method: 'POST',
103
- requestId: 'req_123',
104
- });
171
+ await trackError(
172
+ error, // Error
173
+ { // ErrorTrackingContext
174
+ statusCode: 500,
175
+ path: '/api/example',
176
+ method: 'POST',
177
+ requestId: 'req_123', // optional
178
+ userId: 'user_42', // optional, string
179
+ headers: { /* ... */ }, // optional
180
+ query: { /* ... */ }, // optional
181
+ environment: 'production', // optional
182
+ },
183
+ { custom: 'metadata' }, // optional 3rd arg → error_event.metadata
184
+ );
185
+ ```
186
+
187
+ `ErrorTrackingContext` requires `statusCode`, `path`, `method`; the rest are optional.
188
+
189
+ ### Status management
190
+
191
+ ```typescript
192
+ import { updateErrorGroupStatus } from '@spfn/monitor/server';
193
+
194
+ await updateErrorGroupStatus(groupId, 'resolved'); // sets resolved_at
195
+ await updateErrorGroupStatus(groupId, 'ignored');
196
+ await updateErrorGroupStatus(groupId, 'active'); // reopen
105
197
  ```
106
198
 
107
- ## Developer Logging
199
+ Throws if the group id does not exist. Statuses: `ERROR_GROUP_STATUSES` =
200
+ `['active', 'resolved', 'ignored']` (type `ErrorGroupStatus`).
201
+
202
+ ---
203
+
204
+ ## Developer logging
108
205
 
109
206
  ```typescript
110
- import { writeLog, queryLogs } from '@spfn/monitor/server';
207
+ import { writeLog, queryLogs, monitor } from '@spfn/monitor/server';
111
208
 
112
- // Write a log entry
113
209
  await writeLog({
114
- level: 'info',
210
+ level: 'info', // LogLevel: debug|info|warn|error|fatal
115
211
  message: 'User signed in',
116
- source: 'auth',
117
- userId: 'user_123',
118
- metadata: { provider: 'google' },
212
+ source: 'auth', // optional
213
+ requestId: 'req_123', // optional
214
+ userId: 'user_42', // optional, string
215
+ metadata: { provider: 'google' }, // optional
119
216
  });
120
217
 
121
- // Query logs
122
- const logs = await queryLogs({
218
+ await monitor.log({ level: 'warn', message: 'Rate limit approaching' }); // shorthand → writeLog
219
+
220
+ const logs = await queryLogs({ // LogFilters
123
221
  level: 'error',
124
222
  source: 'payment',
125
- limit: 50,
223
+ search: 'timeout', // ILIKE over message + source
224
+ requestId, userId,
225
+ dateFrom: new Date('2024-01-01'),
226
+ dateTo: new Date(),
227
+ limit: 50, // unbounded if omitted
228
+ offset: 0,
126
229
  });
127
230
  ```
128
231
 
129
- ### Custom Log Store
232
+ `writeLog` / `queryLogs` return `Log` / `Log[]`. `LOG_LEVELS` =
233
+ `['debug','info','warn','error','fatal']` (type `LogLevel`).
234
+
235
+ ### Custom log store
130
236
 
131
- Replace the default DB storage with a custom implementation:
237
+ Swap the default DB store for any backend implementing `LogStore`:
132
238
 
133
239
  ```typescript
134
- import { setLogStore } from '@spfn/monitor/server';
240
+ import { setLogStore, getLogStore, type LogStore } from '@spfn/monitor/server';
135
241
 
136
- setLogStore({
137
- async write(entry) { /* S3, ClickHouse, etc. */ },
138
- async query(filters) { /* ... */ },
139
- async purge(olderThan) { /* ... */ },
140
- });
242
+ const s3Store: LogStore = {
243
+ async write(entry) { /* NewLog Log */ },
244
+ async query(filters) { /* LogFilters → Log[] */ },
245
+ async purge(olderThan) { /* Date → number deleted */ },
246
+ };
247
+
248
+ setLogStore(s3Store);
141
249
  ```
142
250
 
143
- ## Admin API Routes
251
+ `setLogStore` is process-global and affects `writeLog`/`queryLogs` immediately. The default
252
+ store writes to the `logs` table. The admin `GET /_monitor/admin/logs` route also goes
253
+ through the current store.
144
254
 
145
- All routes require `superadmin` role authentication.
255
+ ---
146
256
 
147
- | Method | Path | Description |
148
- |--------|------|-------------|
149
- | GET | `/_monitor/admin/errors` | List error groups (filter by status, path, search) |
150
- | GET | `/_monitor/admin/errors/:id` | Error group detail + recent events |
151
- | PATCH | `/_monitor/admin/errors/:id` | Update error status (resolve/ignore/reopen) |
152
- | GET | `/_monitor/admin/errors/:id/events` | List events for an error group |
153
- | GET | `/_monitor/admin/logs` | Query logs (filter by level, source, search) |
154
- | GET | `/_monitor/admin/stats` | Dashboard statistics |
257
+ ## Admin API routes
155
258
 
156
- ## Dashboard Components
259
+ Mounted by `monitorRouter`. Every route requires authentication **and** the `superadmin`
260
+ role (`authenticate` + `requireRole('superadmin')` from `@spfn/auth/server`).
261
+
262
+ | Method | Path | Query / body | Description |
263
+ |--------|------|--------------|-------------|
264
+ | GET | `/_monitor/admin/errors` | `status, path, search, dateFrom, dateTo, limit(1-100), offset` | List error groups (limit default 20) |
265
+ | GET | `/_monitor/admin/errors/:id` | — | Group detail + recent 20 events |
266
+ | PATCH | `/_monitor/admin/errors/:id` | body `{ status }` | Update status (resolve/ignore/reopen) |
267
+ | GET | `/_monitor/admin/errors/:id/events` | `limit(1-100), offset` | Events for a group (limit default 20) |
268
+ | GET | `/_monitor/admin/logs` | `level, source, search, requestId, userId, dateFrom, dateTo, limit(1-100), offset` | Query logs (limit default 50) |
269
+ | GET | `/_monitor/admin/stats` | — | `MonitorStats` dashboard aggregate |
270
+
271
+ `:id` and `limit`/`offset` are numbers; `dateFrom`/`dateTo` are ISO strings.
272
+
273
+ ### API client
157
274
 
158
275
  ```typescript
159
- // In your Next.js page
276
+ import { monitorApi } from '@spfn/monitor';
277
+
278
+ const stats = await monitorApi.getStats.call({});
279
+ const errors = await monitorApi.listErrors.call({ query: { status: 'active', limit: 20 } });
280
+ const detail = await monitorApi.getErrorDetail.call({ params: { id: 42 } });
281
+ const events = await monitorApi.listErrorEvents.call({ params: { id: 42 }, query: { limit: 50 } });
282
+ const logs = await monitorApi.listLogs.call({ query: { level: 'error' } });
283
+ await monitorApi.updateErrorStatus.call({ params: { id: 42 }, body: { status: 'resolved' } });
284
+ ```
285
+
286
+ `monitorApi` is built with `createApi<typeof monitorRouter>` from `@spfn/core/nextjs`.
287
+ Route keys: `listErrors`, `getErrorDetail`, `updateErrorStatus`, `listErrorEvents`,
288
+ `listLogs`, `getStats`.
289
+
290
+ ### `MonitorStats` shape
291
+
292
+ ```typescript
293
+ interface MonitorStats
294
+ {
295
+ errors: { total: number; active: number; resolved: number; ignored: number };
296
+ recentErrors: ErrorGroup[]; // latest 10 active groups
297
+ logs: { total: number; byLevel: Record<LogLevel, number> };
298
+ trends: { errorsLast24h: number; errorsLast7d: number; logsLast24h: number };
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Dashboard components
305
+
306
+ `'use client'` React components from `@spfn/monitor/nextjs/client`. They fetch through
307
+ `monitorApi` (so the admin routes must be served and the caller must be a superadmin) and
308
+ style with Tailwind CSS (dark-mode aware).
309
+
310
+ ```tsx
311
+ // app/admin/monitor/page.tsx
160
312
  import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
161
313
 
162
- export default function MonitorPage() {
163
- return <MonitorDashboard />;
314
+ export default function MonitorPage()
315
+ {
316
+ return <MonitorDashboard />; // no props — tabs (errors/logs) + stats + drill-down
164
317
  }
165
318
  ```
166
319
 
167
- Available components:
320
+ Individual components and their props:
168
321
 
169
- - `MonitorDashboard` - Full dashboard with tabs (errors, logs) and stats
170
- - `StatsOverview` - Error/log count cards with trends
171
- - `ErrorListView` - Filterable error group table
172
- - `ErrorDetailView` - Error detail with event timeline and status actions
173
- - `LogViewer` - Searchable log list with expandable metadata
322
+ | Component | Props | Notes |
323
+ |-----------|-------|-------|
324
+ | `MonitorDashboard` | | Full dashboard: stats + Errors/Logs tabs + detail drill-down |
325
+ | `StatsOverview` | | Error/log count + trend cards |
326
+ | `ErrorListView` | `onSelect?: (id: number) => void` | Filterable error-group table |
327
+ | `ErrorDetailView` | `errorId: number`, `onBack?: () => void` | Detail + event timeline + status actions |
328
+ | `LogViewer` | — | Searchable log list with expandable metadata |
174
329
 
175
- ## API Client
330
+ ```tsx
331
+ <ErrorListView onSelect={(id) => router.push(`/admin/monitor/errors/${id}`)} />
332
+ <ErrorDetailView errorId={42} onBack={() => router.back()} />
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Pitfalls & anti-patterns
338
+
339
+ - **`mergeRouters` does not exist.** Some older docs/JSDoc show
340
+ `.routes(mergeRouters(appRouter, monitorRouter))` — `@spfn/core/route` exports no such
341
+ function. Mount the monitor router with `.packages([monitorRouter])` on your app router.
342
+ - **Package routes are excluded from `AppRouter` client types.** After
343
+ `.packages([monitorRouter])`, the `/_monitor/admin/*` routes are served but **not** on the
344
+ `api` client — always call them through `monitorApi`, not `api`.
345
+ - **Don't import `/server` or `/config` into client/edge bundles.** `/server` pulls in
346
+ `node:crypto`, DB access, and (via the config side-effect) the env registry. The only
347
+ browser-safe entry points are `@spfn/monitor` (client API + types) and
348
+ `@spfn/monitor/nextjs/client` (components).
349
+ - **`onError` only fires for thrown/error responses ≥ `minStatusCode` (default 500).** 4xx
350
+ results that don't throw aren't tracked. Lower `minStatusCode` (env or option) to widen.
351
+ - **Fingerprint includes `path`.** The *same* error surfacing on two routes becomes two
352
+ groups; a templated path with embedded ids (`/users/123`) fragments groups — normalize the
353
+ path before it reaches tracking if you need them merged.
354
+ - **No automatic retention/purge.** `*_RETENTION_DAYS` are just config values read by getters;
355
+ nothing deletes old rows on its own. Schedule `logStore.purge()` /
356
+ `logsRepository.deleteOlderThan()` (and an equivalent for error events) yourself.
357
+ - **No Slack webhook ⇒ silent skip, not an error.** `notifyErrorToSlack` logs a warning and
358
+ returns when the URL is unset; tracking still persists to the DB.
359
+ - **`setLogStore` is global and unconditional.** It replaces the store for the whole process
360
+ (including the admin log route). There is no per-call store override.
361
+ - **`userId` is a string everywhere here** (`error_events.user_id`, `logs.user_id`,
362
+ `WriteLogParams.userId`). The error handler stringifies the auth context's `userId` for you;
363
+ in manual calls pass a string.
364
+
365
+ ---
366
+
367
+ ## Complete example
176
368
 
177
369
  ```typescript
178
- import { monitorApi } from '@spfn/monitor';
370
+ // src/server/router.ts
371
+ import { defineRouter } from '@spfn/core/route';
372
+ import { authRouter } from '@spfn/auth/server';
373
+ import { monitorRouter } from '@spfn/monitor/server';
374
+
375
+ export const appRouter = defineRouter({ /* app routes */ })
376
+ .packages([authRouter, monitorRouter]);
377
+ ```
179
378
 
180
- // Get dashboard stats
181
- const stats = await monitorApi.getStats.call({});
379
+ ```typescript
380
+ // src/server/server.config.ts
381
+ import { defineServerConfig } from '@spfn/core/server';
382
+ import { createMonitorErrorHandler, createMonitorLifecycle } from '@spfn/monitor/server';
383
+ import { configureMonitor } from '@spfn/monitor/config';
384
+ import { appRouter } from './router';
182
385
 
183
- // List active errors
184
- const errors = await monitorApi.listErrors.call({
185
- query: { status: 'active', limit: 20 },
186
- });
386
+ configureMonitor({ minStatusCode: 500, logRetentionDays: 14 });
187
387
 
188
- // Resolve an error
189
- await monitorApi.updateErrorStatus.call({
190
- params: { id: 1 },
191
- body: { status: 'resolved' },
192
- });
388
+ export default defineServerConfig()
389
+ .middleware({
390
+ onError: createMonitorErrorHandler({ environment: process.env.NODE_ENV }),
391
+ })
392
+ .routes(appRouter)
393
+ .lifecycle(createMonitorLifecycle())
394
+ .build();
193
395
  ```
194
396
 
195
- ## Exports
397
+ ```tsx
398
+ // app/admin/monitor/page.tsx — superadmin-gated route
399
+ import { MonitorDashboard } from '@spfn/monitor/nextjs/client';
400
+
401
+ export default function MonitorPage()
402
+ {
403
+ return <MonitorDashboard />;
404
+ }
405
+ ```
196
406
 
197
407
  ```typescript
198
- // From '@spfn/monitor'
199
- export { monitorApi };
200
- export type { MonitorRouter, MonitorStats, ErrorGroupStatus, LogLevel };
201
-
202
- // From '@spfn/monitor/server'
203
- export {
204
- // Integration
205
- monitorRouter,
206
- createMonitorErrorHandler,
207
- createMonitorLifecycle,
408
+ // anywhere server-side: structured logging
409
+ import { monitor } from '@spfn/monitor/server';
208
410
 
209
- // Services
210
- trackError, updateErrorGroupStatus,
211
- writeLog, queryLogs,
212
- getMonitorStats,
213
- setLogStore,
411
+ await monitor.log({ level: 'info', message: 'job done', source: 'cron', metadata: { took: 120 } });
412
+ ```
214
413
 
215
- // Entities & Repositories
216
- errorGroups, errorEvents, logs,
217
- errorGroupsRepository, errorEventsRepository, logsRepository,
218
- };
414
+ ---
219
415
 
220
- // From '@spfn/monitor/config'
221
- export { configureMonitor };
416
+ ## Exports reference
222
417
 
223
- // From '@spfn/monitor/nextjs/client'
224
- export {
225
- MonitorDashboard, StatsOverview,
226
- ErrorListView, ErrorDetailView, LogViewer,
227
- };
418
+ ```typescript
419
+ // '@spfn/monitor' (client-safe)
420
+ monitorApi
421
+ type MonitorRouter, MonitorStats, ErrorGroupStatus, LogLevel
422
+ ERROR_GROUP_STATUSES, LOG_LEVELS
423
+
424
+ // '@spfn/monitor/server'
425
+ monitorRouter
426
+ createMonitorErrorHandler, createMonitorLifecycle // integration
427
+ trackError, updateErrorGroupStatus, generateFingerprint // error tracking
428
+ writeLog, queryLogs, setLogStore, getLogStore, monitor // logging
429
+ getMonitorStats // stats
430
+ errorGroupsRepository, errorEventsRepository, logsRepository // repositories
431
+ errorGroups, errorEvents, logs, monitorSchema // entities
432
+ ERROR_GROUP_STATUSES, LOG_LEVELS, monitorLogger
433
+ type ErrorTrackingContext, MonitorErrorHandlerOptions, MonitorLifecycleConfig,
434
+ LogStore, WriteLogParams, MonitorStats, ErrorGroupFilters, LogFilters,
435
+ ErrorGroup, NewErrorGroup, ErrorGroupStatus, ErrorEvent, NewErrorEvent,
436
+ Log, NewLog, LogLevel
437
+
438
+ // '@spfn/monitor/config'
439
+ configureMonitor, getMonitorConfig
440
+ getSlackWebhookUrl, getErrorRetentionDays, getLogRetentionDays, getMinStatusCode
441
+ env, monitorEnvSchema
442
+ type MonitorConfig
443
+
444
+ // '@spfn/monitor/nextjs/client'
445
+ MonitorDashboard, StatsOverview, ErrorListView, ErrorDetailView, LogViewer
228
446
  ```
229
447
 
230
- ## License
448
+ ## Related
231
449
 
232
- MIT
450
+ - [@spfn/core/route](../core/src/route/README.md) — `defineRouter`, `.packages()`, route DSL
451
+ - [@spfn/core/server](../core/src/server/README.md) — `defineServerConfig`, `onError` middleware
452
+ - [@spfn/core/env](../core/src/env/README.md) — env schema / registry used by `/config`
453
+ - [@spfn/notification](../notification/README.md) — `sendSlack`, `createErrorSlackNotifier`
454
+ - [@spfn/auth](../auth/README.md) — `authenticate`, `requireRole` guarding the admin routes
@@ -9,6 +9,7 @@ declare const monitorEnvSchema: {
9
9
  required: boolean;
10
10
  examples: string[];
11
11
  type: "string";
12
+ validator: (value: string) => string;
12
13
  } & {
13
14
  key: "SPFN_MONITOR_SLACK_WEBHOOK_URL";
14
15
  };
@@ -50,6 +51,7 @@ declare const env: _spfn_core_env.InferEnvType<{
50
51
  required: boolean;
51
52
  examples: string[];
52
53
  type: "string";
54
+ validator: (value: string) => string;
53
55
  } & {
54
56
  key: "SPFN_MONITOR_SLACK_WEBHOOK_URL";
55
57
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server/entities/schema.ts","../src/server/entities/error-groups.ts","../src/server/entities/error-events.ts","../src/server/entities/logs.ts"],"sourcesContent":["/**\n * @spfn/monitor\n *\n * Error tracking, log management, and monitoring dashboard for SPFN\n *\n * @example\n * ```typescript\n * // Server-side\n * import { monitorRouter, createMonitorErrorHandler } from '@spfn/monitor/server';\n *\n * // Client-side (API calls)\n * import { monitorApi } from '@spfn/monitor';\n * const stats = await monitorApi.getStats.call({});\n * ```\n */\n\n// ============================================================================\n// API Client\n// ============================================================================\nimport { createApi } from '@spfn/core/nextjs';\nimport { monitorRouter } from './server/routes';\n\n/**\n * Type-safe API client for monitor routes\n *\n * @example\n * ```typescript\n * import { monitorApi } from '@spfn/monitor';\n *\n * // Get dashboard stats\n * const stats = await monitorApi.getStats.call({});\n *\n * // List errors\n * const errors = await monitorApi.listErrors.call({\n * query: { status: 'active', limit: 20 }\n * });\n * ```\n */\nexport const monitorApi = createApi<typeof monitorRouter>({});\n\n// Router type for external use\nexport type MonitorRouter = typeof monitorRouter;\n\n// ============================================================================\n// Shared Types (client-safe)\n// ============================================================================\nexport type {\n ErrorGroupStatus,\n LogLevel,\n} from './server/entities';\n\nexport type { MonitorStats } from './server/services/stats.service';\n\nexport {\n ERROR_GROUP_STATUSES,\n LOG_LEVELS,\n} from './server/entities';\n","/**\n * @spfn/monitor - Database Schema Definition\n *\n * Defines the 'spfn_monitor' PostgreSQL schema for all monitor-related tables\n */\n\nimport { createSchema } from '@spfn/core/db';\n\n/**\n * Monitor schema for all monitoring tables\n * Tables: error_groups, error_events, logs\n */\nexport const monitorSchema = createSchema('@spfn/monitor');\n","/**\n * @spfn/monitor - Error Groups Entity\n *\n * Groups errors by fingerprint (name + message + path) to avoid\n * duplicate tracking. Tracks count, status, and first/last seen times.\n */\n\nimport { text, integer, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText, utcTimestamp } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Error group status types\n */\nexport const ERROR_GROUP_STATUSES = ['active', 'resolved', 'ignored'] as const;\nexport type ErrorGroupStatus = typeof ERROR_GROUP_STATUSES[number];\n\n/**\n * Error groups table — groups errors by fingerprint\n */\nexport const errorGroups = monitorSchema.table('error_groups',\n {\n // Primary Key\n id: id(),\n\n // Business Key — SHA-256 first 16 hex chars of (name:message:path)\n fingerprint: text('fingerprint').notNull().unique(),\n\n // Error identification\n name: text('name').notNull(),\n message: text('message').notNull(),\n path: text('path').notNull(),\n method: text('method').notNull(),\n statusCode: integer('status_code').notNull(),\n\n // Status\n status: enumText('status', ERROR_GROUP_STATUSES).default('active').notNull(),\n\n // Counters\n count: integer('count').notNull().default(1),\n\n // Timeline\n firstSeenAt: utcTimestamp('first_seen_at').notNull(),\n lastSeenAt: utcTimestamp('last_seen_at').notNull(),\n resolvedAt: utcTimestamp('resolved_at'),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_eg_fingerprint_idx').on(table.fingerprint),\n index('monitor_eg_status_idx').on(table.status),\n index('monitor_eg_last_seen_at_idx').on(table.lastSeenAt),\n index('monitor_eg_path_idx').on(table.path),\n ]\n);\n\nexport type ErrorGroup = typeof errorGroups.$inferSelect;\nexport type NewErrorGroup = typeof errorGroups.$inferInsert;\n","/**\n * @spfn/monitor - Error Events Entity\n *\n * Individual error occurrences linked to an error group.\n * Stores request-specific context (headers, query, stack trace).\n */\n\nimport { text, integer, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, foreignKey } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\nimport { errorGroups } from './error-groups';\n\n/**\n * Error events table — individual error occurrences\n */\nexport const errorEvents = monitorSchema.table('error_events',\n {\n // Primary Key\n id: id(),\n\n // Foreign Key\n groupId: foreignKey('group', () => errorGroups.id).notNull(),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n statusCode: integer('status_code').notNull(),\n\n // Request details\n headers: jsonb('headers').$type<Record<string, string>>(),\n query: jsonb('query').$type<Record<string, string>>(),\n stackTrace: text('stack_trace'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_ee_group_id_idx').on(table.groupId),\n index('monitor_ee_created_at_idx').on(table.createdAt),\n index('monitor_ee_user_id_idx').on(table.userId),\n ]\n);\n\nexport type ErrorEvent = typeof errorEvents.$inferSelect;\nexport type NewErrorEvent = typeof errorEvents.$inferInsert;\n","/**\n * @spfn/monitor - Logs Entity\n *\n * Developer logs stored in DB for retrieval via admin dashboard.\n * Supports level-based filtering, source tracking, and metadata.\n */\n\nimport { text, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Log level types\n */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = typeof LOG_LEVELS[number];\n\n/**\n * Logs table — developer log entries\n */\nexport const logs = monitorSchema.table('logs',\n {\n // Primary Key\n id: id(),\n\n // Log data\n level: enumText('level', LOG_LEVELS).notNull(),\n message: text('message').notNull(),\n source: text('source'),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n\n // Extra data\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_log_level_idx').on(table.level),\n index('monitor_log_source_idx').on(table.source),\n index('monitor_log_created_at_idx').on(table.createdAt),\n ]\n);\n\nexport type Log = typeof logs.$inferSelect;\nexport type NewLog = typeof logs.$inferInsert;\n"],"mappings":"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","names":["text","integer","index","id","timestamps","id","text","integer","timestamps","index","text","jsonb","index","id","timestamps","enumText","id","enumText","text","jsonb","timestamps","index"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server/entities/schema.ts","../src/server/entities/error-groups.ts","../src/server/entities/error-events.ts","../src/server/entities/logs.ts"],"sourcesContent":["/**\n * @spfn/monitor\n *\n * Error tracking, log management, and monitoring dashboard for SPFN\n *\n * @example\n * ```typescript\n * // Server-side\n * import { monitorRouter, createMonitorErrorHandler } from '@spfn/monitor/server';\n *\n * // Client-side (API calls)\n * import { monitorApi } from '@spfn/monitor';\n * const stats = await monitorApi.getStats.call({});\n * ```\n */\n\n// ============================================================================\n// API Client\n// ============================================================================\nimport { createApi } from '@spfn/core/nextjs';\nimport { monitorRouter } from './server/routes';\n\n/**\n * Type-safe API client for monitor routes\n *\n * @example\n * ```typescript\n * import { monitorApi } from '@spfn/monitor';\n *\n * // Get dashboard stats\n * const stats = await monitorApi.getStats.call({});\n *\n * // List errors\n * const errors = await monitorApi.listErrors.call({\n * query: { status: 'active', limit: 20 }\n * });\n * ```\n */\nexport const monitorApi = createApi<typeof monitorRouter>({});\n\n// Router type for external use\nexport type MonitorRouter = typeof monitorRouter;\n\n// ============================================================================\n// Shared Types (client-safe)\n// ============================================================================\nexport type {\n ErrorGroupStatus,\n LogLevel,\n} from './server/entities';\n\nexport type { MonitorStats } from './server/services/stats.service';\n\nexport {\n ERROR_GROUP_STATUSES,\n LOG_LEVELS,\n} from './server/entities';\n","/**\n * @spfn/monitor - Database Schema Definition\n *\n * Defines the 'spfn_monitor' PostgreSQL schema for all monitor-related tables\n */\n\nimport { createSchema } from '@spfn/core/db';\n\n/**\n * Monitor schema for all monitoring tables\n * Tables: error_groups, error_events, logs\n */\nexport const monitorSchema = createSchema('@spfn/monitor');\n","/**\n * @spfn/monitor - Error Groups Entity\n *\n * Groups errors by fingerprint (name + message + path) to avoid\n * duplicate tracking. Tracks count, status, and first/last seen times.\n */\n\nimport { text, integer, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText, utcTimestamp } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Error group status types\n */\nexport const ERROR_GROUP_STATUSES = ['active', 'resolved', 'ignored'] as const;\nexport type ErrorGroupStatus = typeof ERROR_GROUP_STATUSES[number];\n\n/**\n * Error groups table — groups errors by fingerprint\n */\nexport const errorGroups = monitorSchema.table('error_groups',\n {\n // Primary Key\n id: id(),\n\n // Business Key — SHA-256 first 16 hex chars of (name:message:path)\n fingerprint: text('fingerprint').notNull().unique(),\n\n // Error identification\n name: text('name').notNull(),\n message: text('message').notNull(),\n path: text('path').notNull(),\n method: text('method').notNull(),\n statusCode: integer('status_code').notNull(),\n\n // Status\n status: enumText('status', ERROR_GROUP_STATUSES).default('active').notNull(),\n\n // Counters\n count: integer('count').notNull().default(1),\n\n // Timeline\n firstSeenAt: utcTimestamp('first_seen_at').notNull(),\n lastSeenAt: utcTimestamp('last_seen_at').notNull(),\n resolvedAt: utcTimestamp('resolved_at'),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_eg_fingerprint_idx').on(table.fingerprint),\n index('monitor_eg_status_idx').on(table.status),\n index('monitor_eg_last_seen_at_idx').on(table.lastSeenAt),\n index('monitor_eg_path_idx').on(table.path),\n ],\n);\n\nexport type ErrorGroup = typeof errorGroups.$inferSelect;\nexport type NewErrorGroup = typeof errorGroups.$inferInsert;\n","/**\n * @spfn/monitor - Error Events Entity\n *\n * Individual error occurrences linked to an error group.\n * Stores request-specific context (headers, query, stack trace).\n */\n\nimport { text, integer, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, foreignKey } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\nimport { errorGroups } from './error-groups';\n\n/**\n * Error events table — individual error occurrences\n */\nexport const errorEvents = monitorSchema.table('error_events',\n {\n // Primary Key\n id: id(),\n\n // Foreign Key\n groupId: foreignKey('group', () => errorGroups.id).notNull(),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n statusCode: integer('status_code').notNull(),\n\n // Request details\n headers: jsonb('headers').$type<Record<string, string>>(),\n query: jsonb('query').$type<Record<string, string>>(),\n stackTrace: text('stack_trace'),\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_ee_group_id_idx').on(table.groupId),\n index('monitor_ee_created_at_idx').on(table.createdAt),\n index('monitor_ee_user_id_idx').on(table.userId),\n ],\n);\n\nexport type ErrorEvent = typeof errorEvents.$inferSelect;\nexport type NewErrorEvent = typeof errorEvents.$inferInsert;\n","/**\n * @spfn/monitor - Logs Entity\n *\n * Developer logs stored in DB for retrieval via admin dashboard.\n * Supports level-based filtering, source tracking, and metadata.\n */\n\nimport { text, jsonb, index } from 'drizzle-orm/pg-core';\nimport { id, timestamps, enumText } from '@spfn/core/db';\nimport { monitorSchema } from './schema';\n\n/**\n * Log level types\n */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = typeof LOG_LEVELS[number];\n\n/**\n * Logs table — developer log entries\n */\nexport const logs = monitorSchema.table('logs',\n {\n // Primary Key\n id: id(),\n\n // Log data\n level: enumText('level', LOG_LEVELS).notNull(),\n message: text('message').notNull(),\n source: text('source'),\n\n // Request context\n requestId: text('request_id'),\n userId: text('user_id'),\n\n // Extra data\n metadata: jsonb('metadata').$type<Record<string, unknown>>(),\n\n ...timestamps(),\n },\n (table) => [\n index('monitor_log_level_idx').on(table.level),\n index('monitor_log_source_idx').on(table.source),\n index('monitor_log_created_at_idx').on(table.createdAt),\n ],\n);\n\nexport type Log = typeof logs.$inferSelect;\nexport type NewLog = typeof logs.$inferInsert;\n"],"mappings":"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","names":["text","integer","index","id","timestamps","id","text","integer","timestamps","index","text","jsonb","index","id","timestamps","enumText","id","enumText","text","jsonb","timestamps","index"]}