@spfn/monitor 0.1.0-beta.23 → 0.1.0-beta.25
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.js +28 -19
- 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.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
|
|
|
@@ -2726,6 +2735,7 @@ var logs = monitorSchema.table(
|
|
|
2726
2735
|
);
|
|
2727
2736
|
|
|
2728
2737
|
// src/server/repositories/error-groups.repository.ts
|
|
2738
|
+
var DEFAULT_FIND_LIMIT = 100;
|
|
2729
2739
|
var ErrorGroupsRepository = class extends BaseRepository {
|
|
2730
2740
|
async findById(id4) {
|
|
2731
2741
|
const result = await this.readDb.select().from(errorGroups).where(eq(errorGroups.id, id4)).limit(1);
|
|
@@ -2763,9 +2773,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2763
2773
|
if (where) {
|
|
2764
2774
|
query = query.where(where);
|
|
2765
2775
|
}
|
|
2766
|
-
|
|
2767
|
-
query = query.limit(filters.limit);
|
|
2768
|
-
}
|
|
2776
|
+
query = query.limit(filters.limit ?? DEFAULT_FIND_LIMIT);
|
|
2769
2777
|
if (filters.offset) {
|
|
2770
2778
|
query = query.offset(filters.offset);
|
|
2771
2779
|
}
|
|
@@ -2780,7 +2788,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2780
2788
|
}
|
|
2781
2789
|
async incrementCount(id4) {
|
|
2782
2790
|
const result = await this.db.update(errorGroups).set({
|
|
2783
|
-
count:
|
|
2791
|
+
count: sql2`${errorGroups.count} + 1`,
|
|
2784
2792
|
lastSeenAt: /* @__PURE__ */ new Date(),
|
|
2785
2793
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2786
2794
|
}).where(eq(errorGroups.id, id4)).returning();
|
|
@@ -2797,7 +2805,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2797
2805
|
const result = await this.db.update(errorGroups).set({
|
|
2798
2806
|
status: "active",
|
|
2799
2807
|
resolvedAt: null,
|
|
2800
|
-
count:
|
|
2808
|
+
count: sql2`${errorGroups.count} + 1`,
|
|
2801
2809
|
lastSeenAt: now,
|
|
2802
2810
|
updatedAt: now
|
|
2803
2811
|
}).where(eq(errorGroups.id, id4)).returning();
|
|
@@ -2815,7 +2823,7 @@ var ErrorGroupsRepository = class extends BaseRepository {
|
|
|
2815
2823
|
async countByStatus() {
|
|
2816
2824
|
const result = await this.readDb.select({
|
|
2817
2825
|
status: errorGroups.status,
|
|
2818
|
-
count:
|
|
2826
|
+
count: sql2`count(*)::int`
|
|
2819
2827
|
}).from(errorGroups).groupBy(errorGroups.status);
|
|
2820
2828
|
const counts = {
|
|
2821
2829
|
active: 0,
|
|
@@ -2853,8 +2861,9 @@ var ErrorEventsRepository = class extends BaseRepository2 {
|
|
|
2853
2861
|
var errorEventsRepository = new ErrorEventsRepository();
|
|
2854
2862
|
|
|
2855
2863
|
// src/server/repositories/logs.repository.ts
|
|
2856
|
-
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
|
|
2864
|
+
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";
|
|
2857
2865
|
import { BaseRepository as BaseRepository3 } from "@spfn/core/db";
|
|
2866
|
+
var DEFAULT_FIND_LIMIT2 = 100;
|
|
2858
2867
|
var LogsRepository = class extends BaseRepository3 {
|
|
2859
2868
|
async create(data) {
|
|
2860
2869
|
return await this._create(logs, {
|
|
@@ -2896,9 +2905,7 @@ var LogsRepository = class extends BaseRepository3 {
|
|
|
2896
2905
|
if (where) {
|
|
2897
2906
|
query = query.where(where);
|
|
2898
2907
|
}
|
|
2899
|
-
|
|
2900
|
-
query = query.limit(filters.limit);
|
|
2901
|
-
}
|
|
2908
|
+
query = query.limit(filters.limit ?? DEFAULT_FIND_LIMIT2);
|
|
2902
2909
|
if (filters.offset) {
|
|
2903
2910
|
query = query.offset(filters.offset);
|
|
2904
2911
|
}
|
|
@@ -2907,7 +2914,7 @@ var LogsRepository = class extends BaseRepository3 {
|
|
|
2907
2914
|
async countByLevel() {
|
|
2908
2915
|
const result = await this.readDb.select({
|
|
2909
2916
|
level: logs.level,
|
|
2910
|
-
count:
|
|
2917
|
+
count: sql3`count(*)::int`
|
|
2911
2918
|
}).from(logs).groupBy(logs.level);
|
|
2912
2919
|
const counts = {
|
|
2913
2920
|
debug: 0,
|
|
@@ -3083,8 +3090,10 @@ async function trackError(err, ctx, metadata) {
|
|
|
3083
3090
|
}
|
|
3084
3091
|
return;
|
|
3085
3092
|
}
|
|
3086
|
-
await
|
|
3087
|
-
|
|
3093
|
+
await Promise.all([
|
|
3094
|
+
errorGroupsRepository.incrementCount(existing.id),
|
|
3095
|
+
safeCreateEvent(existing.id, err, ctx, metadata)
|
|
3096
|
+
]);
|
|
3088
3097
|
}
|
|
3089
3098
|
async function updateErrorGroupStatus(groupId, newStatus) {
|
|
3090
3099
|
const group = await errorGroupsRepository.findById(groupId);
|
|
@@ -3157,7 +3166,7 @@ async function queryLogs(filters) {
|
|
|
3157
3166
|
}
|
|
3158
3167
|
|
|
3159
3168
|
// src/server/services/stats.service.ts
|
|
3160
|
-
import { sql as
|
|
3169
|
+
import { sql as sql4, gte as gte3 } from "drizzle-orm";
|
|
3161
3170
|
import { getDatabase } from "@spfn/core/db";
|
|
3162
3171
|
async function getMonitorStats() {
|
|
3163
3172
|
const [statusCounts, recentErrors, levelCounts, trends] = await Promise.all([
|
|
@@ -3187,9 +3196,9 @@ async function getTrends() {
|
|
|
3187
3196
|
const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
3188
3197
|
const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
3189
3198
|
const [errorsLast24h, errorsLast7d, logsLast24h] = await Promise.all([
|
|
3190
|
-
db.select({ count:
|
|
3191
|
-
db.select({ count:
|
|
3192
|
-
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)
|
|
3193
3202
|
]);
|
|
3194
3203
|
return { errorsLast24h, errorsLast7d, logsLast24h };
|
|
3195
3204
|
}
|