@spfn/monitor 0.1.0-beta.22 → 0.1.0-beta.24
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/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +41 -16
- package/dist/server.js.map +1 -1
- package/migrations/0002_magenta_proudstar.sql +7 -0
- package/migrations/meta/0002_snapshot.json +493 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var monitorSchema = createSchema("@spfn/monitor");
|
|
|
7
7
|
|
|
8
8
|
// src/server/entities/error-groups.ts
|
|
9
9
|
import { text, integer, index } from "drizzle-orm/pg-core";
|
|
10
|
+
import { sql } from "drizzle-orm";
|
|
10
11
|
import { id, timestamps, enumText, utcTimestamp } from "@spfn/core/db";
|
|
11
12
|
var ERROR_GROUP_STATUSES = ["active", "resolved", "ignored"];
|
|
12
13
|
var errorGroups = monitorSchema.table(
|
|
@@ -36,7 +37,15 @@ var errorGroups = monitorSchema.table(
|
|
|
36
37
|
index("monitor_eg_fingerprint_idx").on(table.fingerprint),
|
|
37
38
|
index("monitor_eg_status_idx").on(table.status),
|
|
38
39
|
index("monitor_eg_last_seen_at_idx").on(table.lastSeenAt),
|
|
39
|
-
index("monitor_eg_path_idx").on(table.path)
|
|
40
|
+
index("monitor_eg_path_idx").on(table.path),
|
|
41
|
+
// pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE
|
|
42
|
+
// (%term%) on name/message/path sargable instead of a seq scan. Error
|
|
43
|
+
// groups are fingerprint-deduped, so insert volume is low — the write cost
|
|
44
|
+
// of these GIN indexes is acceptable here (NOT applied to the high-volume
|
|
45
|
+
// logs table). Requires the pg_trgm extension (see the migration).
|
|
46
|
+
index("monitor_eg_name_trgm_idx").using("gin", sql`${table.name} gin_trgm_ops`),
|
|
47
|
+
index("monitor_eg_message_trgm_idx").using("gin", sql`${table.message} gin_trgm_ops`),
|
|
48
|
+
index("monitor_eg_path_trgm_idx").using("gin", sql`${table.path} gin_trgm_ops`)
|
|
40
49
|
]
|
|
41
50
|
);
|
|
42
51
|
|
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 { sql } from 'drizzle-orm';\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 // pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE\n // (%term%) on name/message/path sargable instead of a seq scan. Error\n // groups are fingerprint-deduped, so insert volume is low — the write cost\n // of these GIN indexes is acceptable here (NOT applied to the high-volume\n // logs table). Requires the pg_trgm extension (see the migration).\n index('monitor_eg_name_trgm_idx').using('gin', sql`${table.name} gin_trgm_ops`),\n index('monitor_eg_message_trgm_idx').using('gin', sql`${table.message} gin_trgm_ops`),\n index('monitor_eg_path_trgm_idx').using('gin', sql`${table.path} gin_trgm_ops`),\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"]}
|
package/dist/server.d.ts
CHANGED
|
@@ -291,6 +291,13 @@ declare class ErrorGroupsRepository extends BaseRepository {
|
|
|
291
291
|
findMany(filters?: ErrorGroupFilters): Promise<ErrorGroup[]>;
|
|
292
292
|
create(data: NewErrorGroup): Promise<ErrorGroup>;
|
|
293
293
|
incrementCount(id: number): Promise<ErrorGroup | null>;
|
|
294
|
+
/**
|
|
295
|
+
* Reopen a resolved group: set active, clear resolvedAt, and bump count +
|
|
296
|
+
* lastSeenAt in a SINGLE UPDATE (the reopen path used to do updateStatus then
|
|
297
|
+
* incrementCount — two round trips writing the same row, the second clobbering
|
|
298
|
+
* the first).
|
|
299
|
+
*/
|
|
300
|
+
reactivate(id: number): Promise<ErrorGroup | null>;
|
|
294
301
|
updateStatus(id: number, status: ErrorGroupStatus): Promise<ErrorGroup | null>;
|
|
295
302
|
countByStatus(): Promise<Record<ErrorGroupStatus, number>>;
|
|
296
303
|
}
|
package/dist/server.js
CHANGED
|
@@ -2628,7 +2628,7 @@ var Type = type_exports2;
|
|
|
2628
2628
|
import { route } from "@spfn/core/route";
|
|
2629
2629
|
|
|
2630
2630
|
// src/server/repositories/error-groups.repository.ts
|
|
2631
|
-
import { eq, and, desc, sql, ilike, or, gte, lte } from "drizzle-orm";
|
|
2631
|
+
import { eq, and, desc, sql as sql2, ilike, or, gte, lte } from "drizzle-orm";
|
|
2632
2632
|
import { BaseRepository } from "@spfn/core/db";
|
|
2633
2633
|
|
|
2634
2634
|
// src/server/entities/schema.ts
|
|
@@ -2637,6 +2637,7 @@ var monitorSchema = createSchema("@spfn/monitor");
|
|
|
2637
2637
|
|
|
2638
2638
|
// src/server/entities/error-groups.ts
|
|
2639
2639
|
import { text, integer, index } from "drizzle-orm/pg-core";
|
|
2640
|
+
import { sql } from "drizzle-orm";
|
|
2640
2641
|
import { id, timestamps, enumText, utcTimestamp } from "@spfn/core/db";
|
|
2641
2642
|
var ERROR_GROUP_STATUSES = ["active", "resolved", "ignored"];
|
|
2642
2643
|
var errorGroups = monitorSchema.table(
|
|
@@ -2666,7 +2667,15 @@ var errorGroups = monitorSchema.table(
|
|
|
2666
2667
|
index("monitor_eg_fingerprint_idx").on(table.fingerprint),
|
|
2667
2668
|
index("monitor_eg_status_idx").on(table.status),
|
|
2668
2669
|
index("monitor_eg_last_seen_at_idx").on(table.lastSeenAt),
|
|
2669
|
-
index("monitor_eg_path_idx").on(table.path)
|
|
2670
|
+
index("monitor_eg_path_idx").on(table.path),
|
|
2671
|
+
// pg_trgm GIN indexes make the admin search's leading-wildcard ILIKE
|
|
2672
|
+
// (%term%) on name/message/path sargable instead of a seq scan. Error
|
|
2673
|
+
// groups are fingerprint-deduped, so insert volume is low — the write cost
|
|
2674
|
+
// of these GIN indexes is acceptable here (NOT applied to the high-volume
|
|
2675
|
+
// logs table). Requires the pg_trgm extension (see the migration).
|
|
2676
|
+
index("monitor_eg_name_trgm_idx").using("gin", sql`${table.name} gin_trgm_ops`),
|
|
2677
|
+
index("monitor_eg_message_trgm_idx").using("gin", sql`${table.message} gin_trgm_ops`),
|
|
2678
|
+
index("monitor_eg_path_trgm_idx").using("gin", sql`${table.path} gin_trgm_ops`)
|
|
2670
2679
|
]
|
|
2671
2680
|
);
|
|
2672
2681
|
|
|
@@ -2780,12 +2789,29 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2780
2789
|
}
|
|
2781
2790
|
async incrementCount(id4) {
|
|
2782
2791
|
const result = await this.db.update(errorGroups).set({
|
|
2783
|
-
count:
|
|
2792
|
+
count: sql2`${errorGroups.count} + 1`,
|
|
2784
2793
|
lastSeenAt: /* @__PURE__ */ new Date(),
|
|
2785
2794
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2786
2795
|
}).where(eq(errorGroups.id, id4)).returning();
|
|
2787
2796
|
return result[0] ?? null;
|
|
2788
2797
|
}
|
|
2798
|
+
/**
|
|
2799
|
+
* Reopen a resolved group: set active, clear resolvedAt, and bump count +
|
|
2800
|
+
* lastSeenAt in a SINGLE UPDATE (the reopen path used to do updateStatus then
|
|
2801
|
+
* incrementCount — two round trips writing the same row, the second clobbering
|
|
2802
|
+
* the first).
|
|
2803
|
+
*/
|
|
2804
|
+
async reactivate(id4) {
|
|
2805
|
+
const now = /* @__PURE__ */ new Date();
|
|
2806
|
+
const result = await this.db.update(errorGroups).set({
|
|
2807
|
+
status: "active",
|
|
2808
|
+
resolvedAt: null,
|
|
2809
|
+
count: sql2`${errorGroups.count} + 1`,
|
|
2810
|
+
lastSeenAt: now,
|
|
2811
|
+
updatedAt: now
|
|
2812
|
+
}).where(eq(errorGroups.id, id4)).returning();
|
|
2813
|
+
return result[0] ?? null;
|
|
2814
|
+
}
|
|
2789
2815
|
async updateStatus(id4, status) {
|
|
2790
2816
|
const now = /* @__PURE__ */ new Date();
|
|
2791
2817
|
const result = await this.db.update(errorGroups).set({
|
|
@@ -2798,7 +2824,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2798
2824
|
async countByStatus() {
|
|
2799
2825
|
const result = await this.readDb.select({
|
|
2800
2826
|
status: errorGroups.status,
|
|
2801
|
-
count:
|
|
2827
|
+
count: sql2`count(*)::int`
|
|
2802
2828
|
}).from(errorGroups).groupBy(errorGroups.status);
|
|
2803
2829
|
const counts = {
|
|
2804
2830
|
active: 0,
|
|
@@ -2829,14 +2855,14 @@ var ErrorEventsRepository = class extends BaseRepository2 {
|
|
|
2829
2855
|
return await this.readDb.select().from(errorEvents).where(eq2(errorEvents.groupId, groupId)).orderBy(desc2(errorEvents.createdAt)).limit(limit).offset(offset);
|
|
2830
2856
|
}
|
|
2831
2857
|
async deleteOlderThan(date) {
|
|
2832
|
-
const result = await this.db.delete(errorEvents).where(lt(errorEvents.createdAt, date))
|
|
2833
|
-
return result.
|
|
2858
|
+
const result = await this.db.delete(errorEvents).where(lt(errorEvents.createdAt, date));
|
|
2859
|
+
return result.rowCount ?? result.count ?? 0;
|
|
2834
2860
|
}
|
|
2835
2861
|
};
|
|
2836
2862
|
var errorEventsRepository = new ErrorEventsRepository();
|
|
2837
2863
|
|
|
2838
2864
|
// src/server/repositories/logs.repository.ts
|
|
2839
|
-
import { eq as eq3, and as and2, desc as desc3, lt as lt2, ilike as ilike2, or as or2, gte as gte2, lte as lte2, sql as
|
|
2865
|
+
import { eq as eq3, and as and2, desc as desc3, lt as lt2, ilike as ilike2, or as or2, gte as gte2, lte as lte2, sql as sql3 } from "drizzle-orm";
|
|
2840
2866
|
import { BaseRepository as BaseRepository3 } from "@spfn/core/db";
|
|
2841
2867
|
var LogsRepository = class extends BaseRepository3 {
|
|
2842
2868
|
async create(data) {
|
|
@@ -2890,7 +2916,7 @@ var LogsRepository = class extends BaseRepository3 {
|
|
|
2890
2916
|
async countByLevel() {
|
|
2891
2917
|
const result = await this.readDb.select({
|
|
2892
2918
|
level: logs.level,
|
|
2893
|
-
count:
|
|
2919
|
+
count: sql3`count(*)::int`
|
|
2894
2920
|
}).from(logs).groupBy(logs.level);
|
|
2895
2921
|
const counts = {
|
|
2896
2922
|
debug: 0,
|
|
@@ -2905,8 +2931,8 @@ var LogsRepository = class extends BaseRepository3 {
|
|
|
2905
2931
|
return counts;
|
|
2906
2932
|
}
|
|
2907
2933
|
async deleteOlderThan(date) {
|
|
2908
|
-
const result = await this.db.delete(logs).where(lt2(logs.createdAt, date))
|
|
2909
|
-
return result.
|
|
2934
|
+
const result = await this.db.delete(logs).where(lt2(logs.createdAt, date));
|
|
2935
|
+
return result.rowCount ?? result.count ?? 0;
|
|
2910
2936
|
}
|
|
2911
2937
|
};
|
|
2912
2938
|
var logsRepository = new LogsRepository();
|
|
@@ -3053,8 +3079,7 @@ async function trackError(err, ctx, metadata) {
|
|
|
3053
3079
|
return;
|
|
3054
3080
|
}
|
|
3055
3081
|
if (existing.status === "resolved") {
|
|
3056
|
-
await errorGroupsRepository.
|
|
3057
|
-
await errorGroupsRepository.incrementCount(existing.id);
|
|
3082
|
+
await errorGroupsRepository.reactivate(existing.id);
|
|
3058
3083
|
const event = await safeCreateEvent(existing.id, err, ctx, metadata);
|
|
3059
3084
|
logger2.info("Error group reopened", { fingerprint, groupId: existing.id });
|
|
3060
3085
|
if (event) {
|
|
@@ -3141,7 +3166,7 @@ async function queryLogs(filters) {
|
|
|
3141
3166
|
}
|
|
3142
3167
|
|
|
3143
3168
|
// src/server/services/stats.service.ts
|
|
3144
|
-
import { sql as
|
|
3169
|
+
import { sql as sql4, gte as gte3 } from "drizzle-orm";
|
|
3145
3170
|
import { getDatabase } from "@spfn/core/db";
|
|
3146
3171
|
async function getMonitorStats() {
|
|
3147
3172
|
const [statusCounts, recentErrors, levelCounts, trends] = await Promise.all([
|
|
@@ -3171,9 +3196,9 @@ async function getTrends() {
|
|
|
3171
3196
|
const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
3172
3197
|
const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3173
3198
|
const [errorsLast24h, errorsLast7d, logsLast24h] = await Promise.all([
|
|
3174
|
-
db.select({ count:
|
|
3175
|
-
db.select({ count:
|
|
3176
|
-
db.select({ count:
|
|
3199
|
+
db.select({ count: sql4`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last24h)).then((r) => r[0]?.count ?? 0),
|
|
3200
|
+
db.select({ count: sql4`count(*)::int` }).from(errorEvents).where(gte3(errorEvents.createdAt, last7d)).then((r) => r[0]?.count ?? 0),
|
|
3201
|
+
db.select({ count: sql4`count(*)::int` }).from(logs).where(gte3(logs.createdAt, last24h)).then((r) => r[0]?.count ?? 0)
|
|
3177
3202
|
]);
|
|
3178
3203
|
return { errorsLast24h, errorsLast7d, logsLast24h };
|
|
3179
3204
|
}
|