@hasna/logs 0.3.7 → 0.3.9

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/cli/index.js CHANGED
@@ -2379,6 +2379,31 @@ Errors: ${errorCount} Warnings: ${warnCount}`);
2379
2379
  process.exit(0);
2380
2380
  });
2381
2381
  });
2382
+ program2.command("count").description("Count logs with optional breakdown by level or service").option("--project <name|id>", "Project name or ID").option("--service <name>", "Filter by service").option("--level <level>", "Filter by level").option("--since <time>", "Since (1h, 24h, 7d)").option("--until <time>", "Until").option("--group-by <field>", "Breakdown: level | service").action(async (opts) => {
2383
+ const { countLogs } = await import("../count-x3n7qg3c.js");
2384
+ const result = countLogs(getDb(), {
2385
+ project_id: resolveProject(opts.project),
2386
+ service: opts.service,
2387
+ level: opts.level,
2388
+ since: opts.since,
2389
+ until: opts.until,
2390
+ group_by: opts.groupBy
2391
+ });
2392
+ console.log(`Total: ${result.total} ${C.red}Errors: ${result.errors}${C.reset} ${C.yellow}Warns: ${result.warns}${C.reset} Fatals: ${result.fatals}`);
2393
+ if (result.by_service) {
2394
+ console.log(`
2395
+ By Service:`);
2396
+ for (const [svc, cnt] of Object.entries(result.by_service)) {
2397
+ console.log(` ${C.cyan}${pad(svc, 20)}${C.reset} ${cnt}`);
2398
+ }
2399
+ } else if (opts.groupBy === "level") {
2400
+ console.log(`
2401
+ By Level:`);
2402
+ for (const [lvl, cnt] of Object.entries(result.by_level)) {
2403
+ console.log(` ${colorLevel(lvl)} ${cnt}`);
2404
+ }
2405
+ }
2406
+ });
2382
2407
  program2.command("export").description("Export logs to JSON or CSV").option("--project <name|id>", "Project name or ID").option("--since <time>", "Relative time or ISO").option("--level <level>").option("--service <name>").option("--format <fmt>", "json or csv", "json").option("--output <file>", "Output file (default: stdout)").option("--limit <n>", "Max rows", "100000").action(async (opts) => {
2383
2408
  const { exportToCsv, exportToJson } = await import("../export-c3eqjste.js");
2384
2409
  const { createWriteStream } = await import("fs");
@@ -0,0 +1,9 @@
1
+ // @bun
2
+ import {
3
+ countLogs
4
+ } from "./index-edn08m6f.js";
5
+ import"./index-997bkzr2.js";
6
+ import"./index-re3ntm60.js";
7
+ export {
8
+ countLogs
9
+ };
@@ -0,0 +1,51 @@
1
+ // @bun
2
+ import {
3
+ parseTime
4
+ } from "./index-997bkzr2.js";
5
+
6
+ // src/lib/count.ts
7
+ function countLogs(db, opts) {
8
+ const conditions = [];
9
+ const params = {};
10
+ if (opts.project_id) {
11
+ conditions.push("project_id = $p");
12
+ params.$p = opts.project_id;
13
+ }
14
+ if (opts.service) {
15
+ conditions.push("service = $service");
16
+ params.$service = opts.service;
17
+ }
18
+ if (opts.level) {
19
+ conditions.push("level = $level");
20
+ params.$level = opts.level;
21
+ }
22
+ const since = parseTime(opts.since);
23
+ const until = parseTime(opts.until);
24
+ if (since) {
25
+ conditions.push("timestamp >= $since");
26
+ params.$since = since;
27
+ }
28
+ if (until) {
29
+ conditions.push("timestamp <= $until");
30
+ params.$until = until;
31
+ }
32
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
33
+ const byLevel = db.prepare(`SELECT level, COUNT(*) as c FROM logs ${where} GROUP BY level`).all(params);
34
+ const by_level = Object.fromEntries(byLevel.map((r) => [r.level, r.c]));
35
+ const total = byLevel.reduce((s, r) => s + r.c, 0);
36
+ let by_service;
37
+ if (opts.group_by === "service") {
38
+ const bySvc = db.prepare(`SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${where} GROUP BY service ORDER BY c DESC`).all(params);
39
+ by_service = Object.fromEntries(bySvc.map((r) => [r.service, r.c]));
40
+ }
41
+ return {
42
+ total,
43
+ errors: by_level["error"] ?? 0,
44
+ warns: by_level["warn"] ?? 0,
45
+ fatals: by_level["fatal"] ?? 0,
46
+ by_level,
47
+ by_service
48
+ };
49
+ }
50
+
51
+ export { countLogs };
package/dist/mcp/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  import {
4
- countLogs
5
- } from "../index-14dvwcf1.js";
4
+ getHealth
5
+ } from "../index-xjn8gam3.js";
6
6
  import {
7
7
  createAlertRule,
8
8
  createPage,
@@ -35,12 +35,16 @@ import {
35
35
  searchLogs,
36
36
  tailLogs
37
37
  } from "../index-exeq2gs6.js";
38
+ import {
39
+ countLogs
40
+ } from "../index-edn08m6f.js";
38
41
  import {
39
42
  parseTime
40
43
  } from "../index-997bkzr2.js";
41
44
  import {
42
- getHealth
43
- } from "../index-xjn8gam3.js";
45
+ exportToCsv,
46
+ exportToJson
47
+ } from "../index-eh9bkbpa.js";
44
48
  import {
45
49
  __commonJS,
46
50
  __export,
@@ -28618,7 +28622,8 @@ server.tool("log_count", {
28618
28622
  service: exports_external.string().optional(),
28619
28623
  level: exports_external.string().optional(),
28620
28624
  since: exports_external.string().optional(),
28621
- until: exports_external.string().optional()
28625
+ until: exports_external.string().optional(),
28626
+ group_by: exports_external.enum(["level", "service"]).optional().describe("Return breakdown by 'level' or 'service' in addition to totals")
28622
28627
  }, (args) => ({
28623
28628
  content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
28624
28629
  }));
@@ -28653,6 +28658,34 @@ server.tool("log_context_from_id", {
28653
28658
  }, ({ log_id, brief }) => ({
28654
28659
  content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContextFromId(db, log_id), brief !== false)) }]
28655
28660
  }));
28661
+ server.tool("log_export", {
28662
+ project_id: exports_external.string().optional().describe("Project name or ID"),
28663
+ format: exports_external.enum(["json", "csv"]).optional().default("json").describe("Output format"),
28664
+ since: exports_external.string().optional().describe("Since time (1h, 24h, 7d, ISO)"),
28665
+ until: exports_external.string().optional(),
28666
+ level: exports_external.array(exports_external.string()).optional().describe("Filter by levels"),
28667
+ service: exports_external.string().optional(),
28668
+ limit: exports_external.number().optional().default(1e5)
28669
+ }, (args) => {
28670
+ const chunks = [];
28671
+ const write = (s) => {
28672
+ chunks.push(s);
28673
+ return true;
28674
+ };
28675
+ const options = {
28676
+ project_id: rp(args.project_id),
28677
+ level: args.level,
28678
+ service: args.service,
28679
+ since: args.since,
28680
+ until: args.until,
28681
+ limit: args.limit ?? 1e5
28682
+ };
28683
+ if (args.format === "csv")
28684
+ exportToCsv(db, options, write);
28685
+ else
28686
+ exportToJson(db, options, write);
28687
+ return { content: [{ type: "text", text: chunks.join("") }] };
28688
+ });
28656
28689
  server.tool("log_diagnose", {
28657
28690
  project_id: exports_external.string(),
28658
28691
  since: exports_external.string().optional(),
@@ -8,8 +8,8 @@ import {
8
8
  startScheduler
9
9
  } from "../index-7qhh666n.js";
10
10
  import {
11
- countLogs
12
- } from "../index-14dvwcf1.js";
11
+ getHealth
12
+ } from "../index-xjn8gam3.js";
13
13
  import {
14
14
  createAlertRule,
15
15
  createPage,
@@ -43,6 +43,9 @@ import {
43
43
  searchLogs,
44
44
  tailLogs
45
45
  } from "../index-exeq2gs6.js";
46
+ import {
47
+ countLogs
48
+ } from "../index-edn08m6f.js";
46
49
  import {
47
50
  parseTime
48
51
  } from "../index-997bkzr2.js";
@@ -50,9 +53,6 @@ import {
50
53
  exportToCsv,
51
54
  exportToJson
52
55
  } from "../index-eh9bkbpa.js";
53
- import {
54
- getHealth
55
- } from "../index-xjn8gam3.js";
56
56
  import"../index-re3ntm60.js";
57
57
 
58
58
  // node_modules/hono/dist/compose.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/logs",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/cli/index.ts CHANGED
@@ -278,6 +278,39 @@ program.command("watch")
278
278
  process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
279
279
  })
280
280
 
281
+ // ── logs count ────────────────────────────────────────────
282
+ program.command("count")
283
+ .description("Count logs with optional breakdown by level or service")
284
+ .option("--project <name|id>", "Project name or ID")
285
+ .option("--service <name>", "Filter by service")
286
+ .option("--level <level>", "Filter by level")
287
+ .option("--since <time>", "Since (1h, 24h, 7d)")
288
+ .option("--until <time>", "Until")
289
+ .option("--group-by <field>", "Breakdown: level | service")
290
+ .action(async (opts) => {
291
+ const { countLogs } = await import("../lib/count.ts")
292
+ const result = countLogs(getDb(), {
293
+ project_id: resolveProject(opts.project),
294
+ service: opts.service,
295
+ level: opts.level,
296
+ since: opts.since,
297
+ until: opts.until,
298
+ group_by: opts.groupBy as "level" | "service" | undefined,
299
+ })
300
+ console.log(`Total: ${result.total} ${C.red}Errors: ${result.errors}${C.reset} ${C.yellow}Warns: ${result.warns}${C.reset} Fatals: ${result.fatals}`)
301
+ if (result.by_service) {
302
+ console.log(`\nBy Service:`)
303
+ for (const [svc, cnt] of Object.entries(result.by_service)) {
304
+ console.log(` ${C.cyan}${pad(svc, 20)}${C.reset} ${cnt}`)
305
+ }
306
+ } else if (opts.groupBy === "level") {
307
+ console.log(`\nBy Level:`)
308
+ for (const [lvl, cnt] of Object.entries(result.by_level)) {
309
+ console.log(` ${colorLevel(lvl)} ${cnt}`)
310
+ }
311
+ }
312
+ })
313
+
281
314
  // ── logs export ───────────────────────────────────────────
282
315
  program.command("export")
283
316
  .description("Export logs to JSON or CSV")
package/src/lib/count.ts CHANGED
@@ -7,6 +7,7 @@ export interface LogCount {
7
7
  warns: number
8
8
  fatals: number
9
9
  by_level: Record<string, number>
10
+ by_service?: Record<string, number>
10
11
  }
11
12
 
12
13
  export function countLogs(db: Database, opts: {
@@ -15,6 +16,7 @@ export function countLogs(db: Database, opts: {
15
16
  level?: string
16
17
  since?: string
17
18
  until?: string
19
+ group_by?: "level" | "service"
18
20
  }): LogCount {
19
21
  const conditions: string[] = []
20
22
  const params: Record<string, unknown> = {}
@@ -35,11 +37,19 @@ export function countLogs(db: Database, opts: {
35
37
  const by_level = Object.fromEntries(byLevel.map(r => [r.level, r.c]))
36
38
  const total = byLevel.reduce((s, r) => s + r.c, 0)
37
39
 
40
+ let by_service: Record<string, number> | undefined
41
+ if (opts.group_by === "service") {
42
+ const bySvc = db.prepare(`SELECT COALESCE(service, '-') as service, COUNT(*) as c FROM logs ${where} GROUP BY service ORDER BY c DESC`)
43
+ .all(params) as { service: string; c: number }[]
44
+ by_service = Object.fromEntries(bySvc.map(r => [r.service, r.c]))
45
+ }
46
+
38
47
  return {
39
48
  total,
40
49
  errors: by_level["error"] ?? 0,
41
50
  warns: by_level["warn"] ?? 0,
42
51
  fatals: by_level["fatal"] ?? 0,
43
52
  by_level,
53
+ by_service,
44
54
  }
45
55
  }
package/src/mcp/index.ts CHANGED
@@ -13,6 +13,7 @@ import { getLatestSnapshot, getPerfTrend, scoreLabel } from "../lib/perf.ts"
13
13
  import { createAlertRule, deleteAlertRule, listAlertRules } from "../lib/alerts.ts"
14
14
  import { listIssues, updateIssueStatus } from "../lib/issues.ts"
15
15
  import { diagnose } from "../lib/diagnose.ts"
16
+ import { exportToJson, exportToCsv } from "../lib/export.ts"
16
17
  import { compare } from "../lib/compare.ts"
17
18
  import { getHealth } from "../lib/health.ts"
18
19
  import { getSessionContext } from "../lib/session-context.ts"
@@ -154,6 +155,7 @@ server.tool("log_tail", {
154
155
  server.tool("log_count", {
155
156
  project_id: z.string().optional(), service: z.string().optional(),
156
157
  level: z.string().optional(), since: z.string().optional(), until: z.string().optional(),
158
+ group_by: z.enum(["level", "service"]).optional().describe("Return breakdown by 'level' or 'service' in addition to totals"),
157
159
  }, (args) => ({
158
160
  content: [{ type: "text", text: JSON.stringify(countLogs(db, { ...args, project_id: rp(args.project_id) })) }]
159
161
  }))
@@ -188,6 +190,30 @@ server.tool("log_context_from_id", {
188
190
  content: [{ type: "text", text: JSON.stringify(applyBrief(getLogContextFromId(db, log_id), brief !== false)) }]
189
191
  }))
190
192
 
193
+ server.tool("log_export", {
194
+ project_id: z.string().optional().describe("Project name or ID"),
195
+ format: z.enum(["json", "csv"]).optional().default("json").describe("Output format"),
196
+ since: z.string().optional().describe("Since time (1h, 24h, 7d, ISO)"),
197
+ until: z.string().optional(),
198
+ level: z.array(z.string()).optional().describe("Filter by levels"),
199
+ service: z.string().optional(),
200
+ limit: z.number().optional().default(100000),
201
+ }, (args) => {
202
+ const chunks: string[] = []
203
+ const write = (s: string) => { chunks.push(s); return true }
204
+ const options = {
205
+ project_id: rp(args.project_id),
206
+ level: args.level as never,
207
+ service: args.service,
208
+ since: args.since,
209
+ until: args.until,
210
+ limit: args.limit ?? 100000,
211
+ }
212
+ if (args.format === "csv") exportToCsv(db, options, write)
213
+ else exportToJson(db, options, write)
214
+ return { content: [{ type: "text" as const, text: chunks.join("") }] }
215
+ })
216
+
191
217
  server.tool("log_diagnose", {
192
218
  project_id: z.string(),
193
219
  since: z.string().optional(),