@blogic-cz/agent-tools 0.14.8 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.8",
3
+ "version": "0.14.9",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -10,6 +10,7 @@ import { AuditServiceLayer, withAudit } from "#shared/audit";
10
10
  import { VERSION } from "#shared";
11
11
 
12
12
  import { metricsCommand } from "./metrics";
13
+ import { logsCommand } from "./logs";
13
14
  import { traceCommand } from "./trace";
14
15
 
15
16
  const renderCauseToStderr = (cause: Cause.Cause<unknown>) => Console.error(cause.toString());
@@ -18,7 +19,7 @@ const mainCommand = Command.make("observability-tool", {}).pipe(
18
19
  Command.withDescription(
19
20
  "LGTM observability queries — Tempo traces, Loki logs, Prometheus metrics",
20
21
  ),
21
- Command.withSubcommands([traceCommand, metricsCommand]),
22
+ Command.withSubcommands([traceCommand, metricsCommand, logsCommand]),
22
23
  );
23
24
 
24
25
  const cli = Command.run(mainCommand, { version: VERSION });
@@ -0,0 +1,153 @@
1
+ import { Console, Effect } from "effect";
2
+ import { Argument, Command, Flag } from "effect/unstable/cli";
3
+
4
+ import { formatOption, formatOutput } from "#shared";
5
+
6
+ import { ObservabilityToolError } from "./errors";
7
+ import {
8
+ envOption,
9
+ formatObservabilityError,
10
+ observabilityDsQuery,
11
+ profileOption,
12
+ resolveConfig,
13
+ } from "./shared";
14
+
15
+ type LogLine = {
16
+ readonly timestamp: string;
17
+ readonly line: string;
18
+ readonly labels: Record<string, string>;
19
+ };
20
+
21
+ function parseLabel(value: string | Record<string, string>): Record<string, string> {
22
+ if (typeof value === "object" && value !== null) {
23
+ return value;
24
+ }
25
+
26
+ try {
27
+ return JSON.parse(value) as Record<string, string>;
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+
33
+ export function extractLogsFromDsQuery(response: {
34
+ results: {
35
+ A: {
36
+ frames?: Array<{
37
+ schema: { fields: Array<{ name: string; type?: string }> };
38
+ data: { values: unknown[][] };
39
+ }>;
40
+ };
41
+ };
42
+ }): LogLine[] {
43
+ const logs: LogLine[] = [];
44
+
45
+ for (const frame of response.results.A.frames ?? []) {
46
+ const fields = frame.schema.fields;
47
+ const values = frame.data.values;
48
+ const timeIndex = fields.findIndex(
49
+ (field) => field.name === "timestamp" || field.name === "Time" || field.type === "time",
50
+ );
51
+ const lineIndex = fields.findIndex(
52
+ (field) => field.name === "body" || field.name === "Line" || field.name === "line",
53
+ );
54
+ const labelsIndex = fields.findIndex(
55
+ (field) => field.name === "labels" || field.name === "labelTypes",
56
+ );
57
+
58
+ const timestamps = (timeIndex >= 0 ? values[timeIndex] : []) as Array<string | number>;
59
+ const lines = (lineIndex >= 0 ? values[lineIndex] : []) as string[];
60
+ const labelValues = (labelsIndex >= 0 ? values[labelsIndex] : []) as Array<
61
+ string | Record<string, string>
62
+ >;
63
+
64
+ for (const [index, line] of lines.entries()) {
65
+ logs.push({
66
+ timestamp: String(timestamps[index] ?? ""),
67
+ line,
68
+ labels: labelValues[index] ? parseLabel(labelValues[index]) : {},
69
+ });
70
+ }
71
+ }
72
+
73
+ return logs;
74
+ }
75
+
76
+ const queryCommand = Command.make(
77
+ "query",
78
+ {
79
+ logql: Argument.string("logql"),
80
+ format: formatOption,
81
+ env: envOption,
82
+ profile: profileOption,
83
+ start: Flag.string("start").pipe(
84
+ Flag.withDescription("Start time (default: now-1h)"),
85
+ Flag.withDefault("now-1h"),
86
+ ),
87
+ end: Flag.string("end").pipe(
88
+ Flag.withDescription("End time (default: now)"),
89
+ Flag.withDefault("now"),
90
+ ),
91
+ limit: Flag.integer("limit").pipe(
92
+ Flag.withDescription("Max log lines (default: 100)"),
93
+ Flag.withDefault(100),
94
+ ),
95
+ },
96
+ ({ logql, format, env, profile, start, end, limit }) => {
97
+ const startedAt = Date.now();
98
+
99
+ return Effect.gen(function* () {
100
+ const config = yield* resolveConfig(env, profile);
101
+ const response = yield* observabilityDsQuery(config, config.lokiUid, "loki", logql, {
102
+ from: start,
103
+ to: end,
104
+ maxLines: limit,
105
+ });
106
+
107
+ if (response.results.A.error) {
108
+ return yield* new ObservabilityToolError({ cause: new Error(response.results.A.error) });
109
+ }
110
+
111
+ const logs = extractLogsFromDsQuery(response).toSorted((left, right) =>
112
+ right.timestamp.localeCompare(left.timestamp),
113
+ );
114
+
115
+ const result = {
116
+ success: true,
117
+ message: `Found ${logs.length} Loki log line(s)`,
118
+ data: {
119
+ environment: env,
120
+ grafanaUrl: config.url,
121
+ lokiDatasourceUid: config.lokiUid,
122
+ query: logql,
123
+ start,
124
+ end,
125
+ limit,
126
+ logCount: logs.length,
127
+ logs,
128
+ },
129
+ executionTimeMs: Date.now() - startedAt,
130
+ };
131
+
132
+ yield* Console.log(formatOutput(result, format));
133
+ }).pipe(
134
+ Effect.catch((error) =>
135
+ Effect.gen(function* () {
136
+ const result = {
137
+ success: false,
138
+ message: "Failed to execute LogQL query",
139
+ error: formatObservabilityError(error),
140
+ hint: "Check LogQL syntax and Grafana/Loki connectivity",
141
+ executionTimeMs: Date.now() - startedAt,
142
+ };
143
+ yield* Console.log(formatOutput(result, format));
144
+ }),
145
+ ),
146
+ );
147
+ },
148
+ ).pipe(Command.withDescription("Execute LogQL range query via Grafana/Loki"));
149
+
150
+ export const logsCommand = Command.make("logs", {}).pipe(
151
+ Command.withDescription("Loki log operations via Grafana"),
152
+ Command.withSubcommands([queryCommand]),
153
+ );
@@ -13,6 +13,7 @@ import {
13
13
  profileOption,
14
14
  resolveConfig,
15
15
  } from "./shared";
16
+ import { extractLogsFromDsQuery } from "./logs";
16
17
  import type {
17
18
  FlattenedSpan,
18
19
  ObservabilityEnvConfig,
@@ -221,18 +222,6 @@ function summarizeTrace(traceId: string, spans: readonly FlattenedSpan[]): Trace
221
222
  };
222
223
  }
223
224
 
224
- function parseLabel(value: string | Record<string, string>): Record<string, string> {
225
- if (typeof value === "object" && value !== null) {
226
- return value;
227
- }
228
-
229
- try {
230
- return JSON.parse(value) as Record<string, string>;
231
- } catch {
232
- return {};
233
- }
234
- }
235
-
236
225
  type ResolvedTrace = {
237
226
  readonly resolution: SpanResolution;
238
227
  readonly spans: FlattenedSpan[];
@@ -482,34 +471,7 @@ const logsCommand = Command.make(
482
471
  });
483
472
  }
484
473
 
485
- const logs: Array<{ timestamp: string; line: string; labels: Record<string, string> }> = [];
486
- for (const frame of response.results.A.frames ?? []) {
487
- const fields = frame.schema.fields;
488
- const values = frame.data.values;
489
- const timeIndex = fields.findIndex(
490
- (field) => field.name === "timestamp" || field.type === "time",
491
- );
492
- const lineIndex = fields.findIndex(
493
- (field) => field.name === "body" || field.name === "Line" || field.name === "line",
494
- );
495
- const labelsIndex = fields.findIndex(
496
- (field) => field.name === "labels" || field.name === "labelTypes",
497
- );
498
-
499
- const timestamps = (timeIndex >= 0 ? values[timeIndex] : []) as Array<string | number>;
500
- const lines = (lineIndex >= 0 ? values[lineIndex] : []) as string[];
501
- const labelValues = (labelsIndex >= 0 ? values[labelsIndex] : []) as Array<
502
- string | Record<string, string>
503
- >;
504
-
505
- for (const [index, line] of lines.entries()) {
506
- logs.push({
507
- timestamp: String(timestamps[index] ?? ""),
508
- line,
509
- labels: labelValues[index] ? parseLabel(labelValues[index]) : {},
510
- });
511
- }
512
- }
474
+ const logs = extractLogsFromDsQuery(response);
513
475
 
514
476
  const result = {
515
477
  success: true,