@blogic-cz/agent-tools 0.14.6 → 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/README.md +2 -0
- package/package.json +1 -1
- package/schemas/agent-tools.schema.json +8 -0
- package/src/config/loader.ts +2 -0
- package/src/config/types.ts +4 -0
- package/src/observability-tool/index.ts +2 -1
- package/src/observability-tool/logs.ts +153 -0
- package/src/observability-tool/trace.ts +2 -40
- package/src/shared/prerequisites/runtime.ts +20 -2
- package/src/shared/prerequisites/types.ts +3 -3
- package/src/shared/prerequisites/vpn.ts +15 -2
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ bun run agent-tools/example-tool/index.ts ping
|
|
|
178
178
|
// auto defaults to true:
|
|
179
179
|
// darwin -> macos-scutil, linux -> linux-nmcli, win32 -> windows-rasdial
|
|
180
180
|
name: "ExampleVPN",
|
|
181
|
+
// Optional: pass IPSec shared secret to macOS scutil from env without storing the value in config.
|
|
182
|
+
secretEnvVar: "EXAMPLE_VPN_IPSEC_SHARED_SECRET",
|
|
181
183
|
},
|
|
182
184
|
},
|
|
183
185
|
kubernetes: {
|
package/package.json
CHANGED
|
@@ -428,6 +428,10 @@
|
|
|
428
428
|
},
|
|
429
429
|
"serviceName": {
|
|
430
430
|
"type": "string"
|
|
431
|
+
},
|
|
432
|
+
"secretEnvVar": {
|
|
433
|
+
"type": "string",
|
|
434
|
+
"description": "Name of environment variable holding the IPSec shared secret for scutil --nc start."
|
|
431
435
|
}
|
|
432
436
|
},
|
|
433
437
|
"required": ["type"]
|
|
@@ -502,6 +506,10 @@
|
|
|
502
506
|
"leaseTtlMs": {
|
|
503
507
|
"type": "number"
|
|
504
508
|
},
|
|
509
|
+
"secretEnvVar": {
|
|
510
|
+
"type": "string",
|
|
511
|
+
"description": "Name of environment variable holding the VPN shared secret for supported drivers."
|
|
512
|
+
},
|
|
505
513
|
"drivers": {
|
|
506
514
|
"type": "object",
|
|
507
515
|
"additionalProperties": false,
|
package/src/config/loader.ts
CHANGED
|
@@ -30,6 +30,7 @@ const PrerequisitesSchema = Schema.Array(PrerequisiteSchema);
|
|
|
30
30
|
const MacosScutilVpnDriverConfigSchema = Schema.Struct({
|
|
31
31
|
type: Schema.Literal("macos-scutil"),
|
|
32
32
|
serviceName: Schema.optionalKey(Schema.String),
|
|
33
|
+
secretEnvVar: Schema.optionalKey(Schema.String),
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const LinuxNmcliVpnDriverConfigSchema = Schema.Struct({
|
|
@@ -56,6 +57,7 @@ const VpnConfigSchema = Schema.Struct({
|
|
|
56
57
|
disconnectTimeoutMs: Schema.optionalKey(Schema.Number),
|
|
57
58
|
cooldownMs: Schema.optionalKey(Schema.Number),
|
|
58
59
|
leaseTtlMs: Schema.optionalKey(Schema.Number),
|
|
60
|
+
secretEnvVar: Schema.optionalKey(Schema.String),
|
|
59
61
|
drivers: Schema.optionalKey(
|
|
60
62
|
Schema.Struct({
|
|
61
63
|
darwin: Schema.optionalKey(MacosScutilVpnDriverConfigSchema),
|
package/src/config/types.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type VpnPrerequisite = {
|
|
|
16
16
|
export type MacosScutilVpnDriverConfig = {
|
|
17
17
|
type: "macos-scutil";
|
|
18
18
|
serviceName?: string;
|
|
19
|
+
/** Name of environment variable holding the IPSec shared secret for scutil --nc start. */
|
|
20
|
+
secretEnvVar?: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export type LinuxNmcliVpnDriverConfig = {
|
|
@@ -42,6 +44,8 @@ export type VpnConfig = {
|
|
|
42
44
|
disconnectTimeoutMs?: number;
|
|
43
45
|
cooldownMs?: number;
|
|
44
46
|
leaseTtlMs?: number;
|
|
47
|
+
/** Name of environment variable holding the VPN shared secret for supported drivers. */
|
|
48
|
+
secretEnvVar?: string;
|
|
45
49
|
drivers?: {
|
|
46
50
|
darwin?: MacosScutilVpnDriverConfig;
|
|
47
51
|
linux?: LinuxNmcliVpnDriverConfig;
|
|
@@ -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
|
|
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,
|
|
@@ -8,18 +8,25 @@ import { normalizeProfilePrerequisites } from "#shared/prerequisites/config";
|
|
|
8
8
|
import { PrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
9
9
|
import { missingVpnToolHint, resolveVpnDriverConfig } from "#shared/prerequisites/vpn";
|
|
10
10
|
|
|
11
|
+
const readEnv = (name: string) => Bun.env[name];
|
|
12
|
+
|
|
11
13
|
const makeVpnCommand = (driver: ResolvedVpnDriver, action: "status" | "start" | "stop") => {
|
|
12
14
|
if (driver.type === "macos-scutil") {
|
|
15
|
+
const secret = driver.secretEnvVar ? readEnv(driver.secretEnvVar) : undefined;
|
|
16
|
+
const secretArgs = action === "start" && secret ? ["--secret", secret] : [];
|
|
17
|
+
const redactedSecretArgs = secretArgs.length > 0 ? ["--secret", "<redacted>"] : [];
|
|
13
18
|
const args =
|
|
14
19
|
action === "status"
|
|
15
20
|
? ["--nc", "status", driver.serviceName]
|
|
16
21
|
: action === "start"
|
|
17
|
-
? ["--nc", "start", driver.serviceName]
|
|
22
|
+
? ["--nc", "start", driver.serviceName, ...secretArgs]
|
|
18
23
|
: ["--nc", "stop", driver.serviceName];
|
|
24
|
+
const labelArgs =
|
|
25
|
+
action === "start" ? ["--nc", "start", driver.serviceName, ...redactedSecretArgs] : args;
|
|
19
26
|
|
|
20
27
|
return {
|
|
21
28
|
command: ChildProcess.make("scutil", args, { stdout: "pipe", stderr: "pipe" }),
|
|
22
|
-
label: ["scutil", ...
|
|
29
|
+
label: ["scutil", ...labelArgs].join(" "),
|
|
23
30
|
};
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -163,6 +170,17 @@ export const runWithProfilePrerequisites = <A, E, CommandError>(
|
|
|
163
170
|
continue;
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
if (
|
|
174
|
+
driver.type === "macos-scutil" &&
|
|
175
|
+
driver.secretEnvVar &&
|
|
176
|
+
!readEnv(driver.secretEnvVar)
|
|
177
|
+
) {
|
|
178
|
+
return yield* new PrerequisiteRunError({
|
|
179
|
+
message: `VPN secret environment variable "${driver.secretEnvVar}" is not set.`,
|
|
180
|
+
hint: `Set ${driver.secretEnvVar} before running this tool or remove secretEnvVar from the VPN config.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
const startCommand = makeVpnCommand(driver, "start");
|
|
167
185
|
const startResult = yield* runCommand(startCommand.command, startCommand.label).pipe(
|
|
168
186
|
Effect.mapError(
|
|
@@ -15,9 +15,9 @@ export type PrerequisiteResolution =
|
|
|
15
15
|
export type SupportedPlatform = "darwin" | "linux" | "win32";
|
|
16
16
|
|
|
17
17
|
export type ResolvedVpnDriver =
|
|
18
|
-
| (
|
|
19
|
-
| (
|
|
20
|
-
| (
|
|
18
|
+
| (MacosScutilVpnDriverConfig & { platform: "darwin"; serviceName: string })
|
|
19
|
+
| (LinuxNmcliVpnDriverConfig & { platform: "linux"; connectionName: string })
|
|
20
|
+
| (WindowsRasdialVpnDriverConfig & { platform: "win32"; entryName: string });
|
|
21
21
|
|
|
22
22
|
export type VpnDriverResolution =
|
|
23
23
|
| { success: true; driver: ResolvedVpnDriver }
|
|
@@ -24,7 +24,12 @@ const resolveExplicitDriver = (
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
success: true,
|
|
27
|
-
driver: {
|
|
27
|
+
driver: {
|
|
28
|
+
platform,
|
|
29
|
+
type: driver.type,
|
|
30
|
+
serviceName: driver.serviceName ?? config.name,
|
|
31
|
+
secretEnvVar: driver.secretEnvVar ?? config.secretEnvVar,
|
|
32
|
+
},
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -93,7 +98,15 @@ export function resolveVpnDriverConfig(
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
if (platform === "darwin") {
|
|
96
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
driver: {
|
|
104
|
+
platform,
|
|
105
|
+
type: "macos-scutil",
|
|
106
|
+
serviceName: config.name,
|
|
107
|
+
secretEnvVar: config.secretEnvVar,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
if (platform === "linux") {
|