@blogic-cz/agent-tools 0.9.0 → 0.11.0
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 +40 -20
- package/package.json +13 -1
- package/schemas/agent-tools.schema.json +48 -1
- package/src/config/index.ts +2 -0
- package/src/config/loader.ts +17 -1
- package/src/config/types.ts +15 -0
- package/src/gh-tool/index.ts +2 -0
- package/src/gh-tool/pr/commands.ts +39 -0
- package/src/gh-tool/pr/core.ts +22 -0
- package/src/gh-tool/pr/index.ts +1 -0
- package/src/grafana-tool/alerts.ts +159 -0
- package/src/grafana-tool/dashboards.ts +151 -0
- package/src/grafana-tool/datasources.ts +72 -0
- package/src/grafana-tool/errors.ts +8 -0
- package/src/grafana-tool/health.ts +57 -0
- package/src/grafana-tool/index.ts +42 -0
- package/src/grafana-tool/logs.ts +124 -0
- package/src/grafana-tool/metrics.ts +217 -0
- package/src/grafana-tool/shared.ts +194 -0
- package/src/grafana-tool/types.ts +29 -0
- package/src/index.ts +9 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Flag } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
import { ConfigService, getToolConfig } from "#config";
|
|
5
|
+
import type { GrafanaConfig } from "#config";
|
|
6
|
+
|
|
7
|
+
import { GrafanaToolError } from "./errors";
|
|
8
|
+
import type { DsQueryOpts, DsQueryResponse, GrafanaEnvConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_LOCAL_URL = "http://localhost:40300";
|
|
11
|
+
const DEFAULT_PROMETHEUS_UID = "prometheus";
|
|
12
|
+
const DEFAULT_LOKI_UID = "loki";
|
|
13
|
+
export function formatGrafanaError(error: unknown): string {
|
|
14
|
+
if (error instanceof GrafanaToolError) {
|
|
15
|
+
return formatGrafanaError(error.cause);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
return error.message;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return String(error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const envOption = Flag.string("env").pipe(
|
|
26
|
+
Flag.withDescription("Target environment name from agent-tools config (default: local)"),
|
|
27
|
+
Flag.withDefault("local"),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const profileOption = Flag.optional(
|
|
31
|
+
Flag.string("profile").pipe(
|
|
32
|
+
Flag.withDescription(
|
|
33
|
+
"Grafana profile name from agent-tools config (default: 'default' key or single entry)",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function resolveToken(tokenEnvVar?: string): string | undefined {
|
|
39
|
+
if (!tokenEnvVar) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const token = process.env[tokenEnvVar];
|
|
44
|
+
if (!token) {
|
|
45
|
+
throw new Error(`${tokenEnvVar} environment variable is not set`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return token;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveFromProfile(
|
|
52
|
+
profile: GrafanaConfig | undefined,
|
|
53
|
+
env: string,
|
|
54
|
+
): GrafanaEnvConfig | undefined {
|
|
55
|
+
const environment = profile?.environments[env];
|
|
56
|
+
if (!environment) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
url: environment.url,
|
|
62
|
+
token: resolveToken(environment.tokenEnvVar),
|
|
63
|
+
prometheusUid: environment.prometheusUid ?? DEFAULT_PROMETHEUS_UID,
|
|
64
|
+
lokiUid: environment.lokiUid ?? DEFAULT_LOKI_UID,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveFromEnv(env: string): GrafanaEnvConfig {
|
|
69
|
+
if (env === "local") {
|
|
70
|
+
return {
|
|
71
|
+
url: process.env.GRAFANA_URL_LOCAL ?? DEFAULT_LOCAL_URL,
|
|
72
|
+
token: process.env.GRAFANA_TOKEN_LOCAL,
|
|
73
|
+
prometheusUid: DEFAULT_PROMETHEUS_UID,
|
|
74
|
+
lokiUid: DEFAULT_LOKI_UID,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const upper = env.toUpperCase();
|
|
79
|
+
const url = process.env[`GRAFANA_URL_${upper}`];
|
|
80
|
+
const token = process.env[`GRAFANA_TOKEN_${upper}`];
|
|
81
|
+
|
|
82
|
+
if (!url) {
|
|
83
|
+
throw new Error(`No grafana.${env} config found and GRAFANA_URL_${upper} is not set`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!token) {
|
|
87
|
+
throw new Error(`No grafana.${env} config found and GRAFANA_TOKEN_${upper} is not set`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
url,
|
|
92
|
+
token,
|
|
93
|
+
prometheusUid: DEFAULT_PROMETHEUS_UID,
|
|
94
|
+
lokiUid: DEFAULT_LOKI_UID,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const resolveConfig = (env: string, profile: Option.Option<string>) =>
|
|
99
|
+
Effect.gen(function* () {
|
|
100
|
+
const config = yield* ConfigService;
|
|
101
|
+
const profileName = Option.getOrUndefined(profile);
|
|
102
|
+
const grafanaConfig = getToolConfig<GrafanaConfig>(config, "grafana", profileName);
|
|
103
|
+
|
|
104
|
+
return yield* Effect.try({
|
|
105
|
+
try: () => resolveFromProfile(grafanaConfig, env) ?? resolveFromEnv(env),
|
|
106
|
+
catch: (cause) => new GrafanaToolError({ cause }),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export function buildHeaders(token?: string): Headers {
|
|
111
|
+
const headers = new Headers({
|
|
112
|
+
Accept: "application/json",
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (token) {
|
|
117
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return headers;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function grafanaFetch<T>(
|
|
124
|
+
config: GrafanaEnvConfig,
|
|
125
|
+
path: string,
|
|
126
|
+
init?: RequestInit,
|
|
127
|
+
): Effect.Effect<T, GrafanaToolError> {
|
|
128
|
+
return Effect.tryPromise({
|
|
129
|
+
try: async () => {
|
|
130
|
+
const headers = buildHeaders(config.token);
|
|
131
|
+
if (init?.headers) {
|
|
132
|
+
const extraHeaders = new Headers(init.headers);
|
|
133
|
+
extraHeaders.forEach((value, key) => {
|
|
134
|
+
headers.set(key, value);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const response = await fetch(`${config.url}${path}`, {
|
|
139
|
+
...init,
|
|
140
|
+
headers,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const body = await response.text().catch(() => "");
|
|
145
|
+
throw new Error(`Grafana API ${response.status}: ${path} — ${body}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (await response.json()) as T;
|
|
149
|
+
},
|
|
150
|
+
catch: (cause) => new GrafanaToolError({ cause }),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function grafanaDsQuery(
|
|
155
|
+
config: GrafanaEnvConfig,
|
|
156
|
+
datasourceUid: string,
|
|
157
|
+
datasourceType: "prometheus" | "loki",
|
|
158
|
+
expr: string,
|
|
159
|
+
options?: DsQueryOpts,
|
|
160
|
+
): Effect.Effect<DsQueryResponse, GrafanaToolError> {
|
|
161
|
+
const opts = options ?? {};
|
|
162
|
+
const query: Record<string, unknown> = {
|
|
163
|
+
refId: "A",
|
|
164
|
+
datasource: { uid: datasourceUid, type: datasourceType },
|
|
165
|
+
expr,
|
|
166
|
+
intervalMs: opts.intervalMs ?? 1000,
|
|
167
|
+
maxDataPoints: opts.maxDataPoints ?? 1000,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (datasourceType === "prometheus") {
|
|
171
|
+
const instant = opts.instant ?? true;
|
|
172
|
+
query.instant = instant;
|
|
173
|
+
query.range = !instant;
|
|
174
|
+
if (opts.step) {
|
|
175
|
+
query.intervalMs = opts.step * 1000;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (datasourceType === "loki") {
|
|
180
|
+
query.queryType = "range";
|
|
181
|
+
if (opts.maxLines) {
|
|
182
|
+
query.maxLines = opts.maxLines;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return grafanaFetch<DsQueryResponse>(config, "/api/ds/query", {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
queries: [query],
|
|
190
|
+
from: opts.from ?? "now-5m",
|
|
191
|
+
to: opts.to ?? "now",
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type GrafanaEnvConfig = {
|
|
2
|
+
url: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
prometheusUid: string;
|
|
5
|
+
lokiUid: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DsQueryOpts = {
|
|
9
|
+
instant?: boolean;
|
|
10
|
+
from?: string;
|
|
11
|
+
to?: string;
|
|
12
|
+
maxLines?: number;
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
maxDataPoints?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DsQueryResponse = {
|
|
19
|
+
results: {
|
|
20
|
+
A: {
|
|
21
|
+
status?: number;
|
|
22
|
+
frames?: Array<{
|
|
23
|
+
schema: { fields: Array<{ name: string; type: string }> };
|
|
24
|
+
data: { values: unknown[][] };
|
|
25
|
+
}>;
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export type { AgentToolsConfig } from "./config/index";
|
|
1
|
+
export type { AgentToolsConfig, GrafanaConfig, GrafanaEnvTarget } from "./config/index";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
AuditService,
|
|
5
|
+
AuditServiceLayer,
|
|
6
|
+
makeAuditServiceLayer,
|
|
7
|
+
resolveAuditDbPath,
|
|
8
|
+
withAudit,
|
|
9
|
+
} from "./shared/audit";
|