@arcote.tech/arc-cli 0.7.18 → 0.7.20
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 +651 -105
- package/package.json +9 -9
- package/src/deploy/bootstrap.ts +8 -3
- package/src/deploy/caddyfile.ts +43 -8
- package/src/deploy/compose.ts +73 -0
- package/src/deploy/config.ts +15 -0
- package/src/deploy/observability-configs.ts +674 -48
- package/src/platform/server.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.20",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform --external '@opentelemetry/*' && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.7.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.7.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.
|
|
20
|
-
"@arcote.tech/arc-adapter-db-postgres": "^0.7.
|
|
21
|
-
"@arcote.tech/arc-otel": "^0.7.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.20",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.20",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.20",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.20",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.20",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.7.20",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.7.20",
|
|
22
22
|
"@opentelemetry/api": "^1.9.0",
|
|
23
23
|
"@opentelemetry/api-logs": "^0.57.0",
|
|
24
24
|
"@opentelemetry/core": "^1.30.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
32
32
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
|
-
"@arcote.tech/platform": "^0.7.
|
|
34
|
+
"@arcote.tech/platform": "^0.7.20",
|
|
35
35
|
"@clack/prompts": "^0.9.0",
|
|
36
36
|
"commander": "^11.1.0",
|
|
37
37
|
"chokidar": "^3.5.3",
|
package/src/deploy/bootstrap.ts
CHANGED
|
@@ -133,6 +133,9 @@ export async function bootstrap(inputs: BootstrapInputs): Promise<void> {
|
|
|
133
133
|
// - stack isn't fully ready, OR
|
|
134
134
|
// - marker is missing (legacy v0.5 deploy with no .arc-state.json), OR
|
|
135
135
|
// - configHash differs from last bootstrap (deploy.arc.json changed), OR
|
|
136
|
+
// - the CLI version changed (generators evolve — compose/Caddyfile/
|
|
137
|
+
// observability configs must be re-rendered + re-uploaded even when
|
|
138
|
+
// deploy.arc.json itself is unchanged), OR
|
|
136
139
|
// - registry container isn't running (e.g. legacy stack predates v0.7)
|
|
137
140
|
// Without this, an old v0.5 stack (no registry container) is classified as
|
|
138
141
|
// "ready" and bootstrap is skipped — then `docker login` on the next step
|
|
@@ -141,6 +144,7 @@ export async function bootstrap(inputs: BootstrapInputs): Promise<void> {
|
|
|
141
144
|
state.kind !== "ready" ||
|
|
142
145
|
state.marker === null ||
|
|
143
146
|
state.marker.configHash !== inputs.configHash ||
|
|
147
|
+
state.marker.cliVersion !== inputs.cliVersion ||
|
|
144
148
|
!(await isRegistryRunning(cfg));
|
|
145
149
|
|
|
146
150
|
if (needUpStack) {
|
|
@@ -156,7 +160,7 @@ export async function bootstrap(inputs: BootstrapInputs): Promise<void> {
|
|
|
156
160
|
// without forcing a full bootstrap.
|
|
157
161
|
if (cfg.observability?.enabled) {
|
|
158
162
|
log("Ensuring observability sidecars are running...");
|
|
159
|
-
const obsServices = ["otel-collector", "tempo", "loki", "prometheus", "grafana"];
|
|
163
|
+
const obsServices = ["otel-collector", "tempo", "loki", "prometheus", "alloy", "grafana"];
|
|
160
164
|
await assertExec(
|
|
161
165
|
cfg.target,
|
|
162
166
|
`cd ${cfg.target.remoteDir} && docker compose pull --ignore-pull-failures ${obsServices.join(" ")} && docker compose up -d ${obsServices.join(" ")}`,
|
|
@@ -307,10 +311,11 @@ async function upStack(inputs: BootstrapInputs): Promise<void> {
|
|
|
307
311
|
// `observability/` directory referenced by compose bind-mounts.
|
|
308
312
|
if (observabilityFiles && observabilityHtpasswd) {
|
|
309
313
|
// Make sure both the top-level `observability/` dir and the nested
|
|
310
|
-
// `grafana-dashboards/` exist before scp tries
|
|
314
|
+
// `grafana-dashboards/` + `grafana-alerting/` exist before scp tries
|
|
315
|
+
// to land files there.
|
|
311
316
|
await assertExec(
|
|
312
317
|
cfg.target,
|
|
313
|
-
`mkdir -p ${cfg.target.remoteDir}/observability/grafana-dashboards`,
|
|
318
|
+
`mkdir -p ${cfg.target.remoteDir}/observability/grafana-dashboards ${cfg.target.remoteDir}/observability/grafana-alerting`,
|
|
314
319
|
);
|
|
315
320
|
// Make sure local nested dir exists too — `generateObservabilityConfigs`
|
|
316
321
|
// returns relative paths like `observability/grafana-dashboards/x.json`
|
package/src/deploy/caddyfile.ts
CHANGED
|
@@ -20,23 +20,48 @@ export function generateCaddyfile(cfg: DeployConfig): string {
|
|
|
20
20
|
cfg.caddy.email === "internal" ? "" : `\n email ${cfg.caddy.email}`;
|
|
21
21
|
const tlsDirective =
|
|
22
22
|
cfg.caddy.email === "internal" ? "\n tls internal" : "";
|
|
23
|
+
const observability = cfg.observability?.enabled === true;
|
|
24
|
+
|
|
25
|
+
// Access logs land on stdout as JSON → Alloy tails the container → Loki
|
|
26
|
+
// (`{compose_service="caddy"}`). Off by default in Caddy, hence per-vhost.
|
|
27
|
+
const logDirective = observability
|
|
28
|
+
? [" log {", " output stdout", " format json", " }"]
|
|
29
|
+
: [];
|
|
23
30
|
|
|
24
31
|
const lines: string[] = [];
|
|
25
32
|
lines.push("# Generated by `arc platform deploy` — do not edit by hand.");
|
|
26
33
|
lines.push("");
|
|
27
34
|
lines.push("{");
|
|
28
35
|
lines.push(" admin off");
|
|
36
|
+
if (observability) {
|
|
37
|
+
// Per-request HTTP metrics (caddy_http_*). Top-level global option —
|
|
38
|
+
// requires Caddy >= 2.9 (the `servers > metrics` form is deprecated).
|
|
39
|
+
lines.push(" metrics {");
|
|
40
|
+
lines.push(" per_host");
|
|
41
|
+
lines.push(" }");
|
|
42
|
+
}
|
|
29
43
|
if (email) lines.push(` ${email.trim()}`);
|
|
30
44
|
lines.push("}");
|
|
31
45
|
lines.push("");
|
|
32
46
|
|
|
47
|
+
// Exposition endpoint for Prometheus (scrapes caddy:2020 on arc-net).
|
|
48
|
+
// Plain HTTP, never exposed publicly; works with `admin off` (only the
|
|
49
|
+
// admin-API /metrics endpoint dies with the admin interface).
|
|
50
|
+
if (observability) {
|
|
51
|
+
lines.push(":2020 {");
|
|
52
|
+
lines.push(" metrics");
|
|
53
|
+
lines.push("}");
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
// Public blocks — one per env. When observability is on, add a `/otel/*`
|
|
34
58
|
// path that forwards browser-side OTLP/HTTP to the collector. Strips the
|
|
35
59
|
// /otel prefix so the collector sees the same /v1/{traces,logs,metrics}
|
|
36
60
|
// paths it would receive from same-network senders.
|
|
37
61
|
for (const [name, env] of Object.entries(cfg.envs)) {
|
|
38
62
|
lines.push(`${env.domain} {${tlsDirective}`);
|
|
39
|
-
|
|
63
|
+
lines.push(...logDirective);
|
|
64
|
+
if (observability) {
|
|
40
65
|
lines.push(" handle_path /otel/* {");
|
|
41
66
|
lines.push(" reverse_proxy otel-collector:4318");
|
|
42
67
|
lines.push(" }");
|
|
@@ -54,13 +79,11 @@ export function generateCaddyfile(cfg: DeployConfig): string {
|
|
|
54
79
|
// the apex of the first env's domain (e.g. observability.app.example.com
|
|
55
80
|
// when the primary env is app.example.com). Caddy issues a separate
|
|
56
81
|
// ACME certificate for this hostname.
|
|
57
|
-
if (
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const observabilityDomain = `${subdomain}.${apex}`;
|
|
63
|
-
lines.push(`${observabilityDomain} {${tlsDirective}`);
|
|
82
|
+
if (observability) {
|
|
83
|
+
const domain = observabilityDomain(cfg);
|
|
84
|
+
if (domain) {
|
|
85
|
+
lines.push(`${domain} {${tlsDirective}`);
|
|
86
|
+
lines.push(...logDirective);
|
|
64
87
|
// Basic-auth credentials live in the same htpasswd file used for the
|
|
65
88
|
// registry — bootstrap appends an "admin" line with bcrypted password.
|
|
66
89
|
lines.push(" basic_auth {");
|
|
@@ -77,6 +100,7 @@ export function generateCaddyfile(cfg: DeployConfig): string {
|
|
|
77
100
|
// 5 GiB request body cap fits real app images comfortably (default 100MB
|
|
78
101
|
// triggers 413 on the first layer push).
|
|
79
102
|
lines.push(`${cfg.registry.domain} {${tlsDirective}`);
|
|
103
|
+
lines.push(...logDirective);
|
|
80
104
|
lines.push(" reverse_proxy registry:5000 {");
|
|
81
105
|
lines.push(" header_up Host {host}");
|
|
82
106
|
lines.push(" }");
|
|
@@ -88,6 +112,17 @@ export function generateCaddyfile(cfg: DeployConfig): string {
|
|
|
88
112
|
return lines.join("\n") + "\n";
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
/** Public hostname of the Grafana UI (`<subdomain>.<apex-of-first-env>`), or
|
|
116
|
+
* null when observability is off / no envs exist. Shared by the Caddyfile
|
|
117
|
+
* vhost and Grafana's GF_SERVER_ROOT_URL (compose.ts). */
|
|
118
|
+
export function observabilityDomain(cfg: DeployConfig): string | null {
|
|
119
|
+
if (!cfg.observability?.enabled) return null;
|
|
120
|
+
const firstEnv = Object.values(cfg.envs)[0];
|
|
121
|
+
if (!firstEnv) return null;
|
|
122
|
+
const subdomain = cfg.observability.subdomain ?? "observability";
|
|
123
|
+
return `${subdomain}.${apexOf(firstEnv.domain)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
/** Apex of a (possibly-subdomain) host. `app.example.com` → `example.com`,
|
|
92
127
|
* `example.com` → `example.com`, `example.co.uk` → `co.uk` (approximate —
|
|
93
128
|
* good enough for the observability subdomain). */
|
package/src/deploy/compose.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observabilityDomain } from "./caddyfile";
|
|
1
2
|
import type { DeployConfig } from "./config";
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +22,17 @@ export interface ComposeOptions {
|
|
|
21
22
|
cfg: DeployConfig;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** json-file log rotation for every service — without it container logs grow
|
|
26
|
+
* unbounded and eventually fill the disk. Alloy tails these same files via
|
|
27
|
+
* the Docker API, and rotation is handled transparently. */
|
|
28
|
+
function pushLogging(lines: string[]): void {
|
|
29
|
+
lines.push(" logging:");
|
|
30
|
+
lines.push(" driver: json-file");
|
|
31
|
+
lines.push(" options:");
|
|
32
|
+
lines.push(' max-size: "10m"');
|
|
33
|
+
lines.push(' max-file: "3"');
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export function generateCompose({ cfg }: ComposeOptions): string {
|
|
25
37
|
const lines: string[] = [];
|
|
26
38
|
lines.push("# Generated by `arc platform deploy` — do not edit by hand.");
|
|
@@ -29,6 +41,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
29
41
|
lines.push(" caddy:");
|
|
30
42
|
lines.push(" image: caddy:2-alpine");
|
|
31
43
|
lines.push(" restart: unless-stopped");
|
|
44
|
+
pushLogging(lines);
|
|
32
45
|
lines.push(" ports:");
|
|
33
46
|
lines.push(' - "80:80"');
|
|
34
47
|
lines.push(' - "443:443"');
|
|
@@ -46,6 +59,10 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
46
59
|
lines.push(" - caddy_config:/config");
|
|
47
60
|
lines.push(" networks:");
|
|
48
61
|
lines.push(" - arc-net");
|
|
62
|
+
if (cfg.observability?.enabled) {
|
|
63
|
+
lines.push(" expose:");
|
|
64
|
+
lines.push(' - "2020" # Prometheus metrics endpoint (Caddyfile :2020 site)');
|
|
65
|
+
}
|
|
49
66
|
lines.push("");
|
|
50
67
|
|
|
51
68
|
// Private Docker Registry — `arc platform deploy` pushes app images here,
|
|
@@ -54,6 +71,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
54
71
|
lines.push(" registry:");
|
|
55
72
|
lines.push(" image: registry:2");
|
|
56
73
|
lines.push(" restart: unless-stopped");
|
|
74
|
+
pushLogging(lines);
|
|
57
75
|
lines.push(" volumes:");
|
|
58
76
|
lines.push(" - registry_data:/var/lib/registry");
|
|
59
77
|
lines.push(" - ./registry-auth/htpasswd:/auth/htpasswd:ro");
|
|
@@ -81,6 +99,15 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
81
99
|
);
|
|
82
100
|
lines.push(` container_name: arc-${name}`);
|
|
83
101
|
lines.push(" restart: unless-stopped");
|
|
102
|
+
pushLogging(lines);
|
|
103
|
+
// The host server exposes GET /health (packages/host healthHandler).
|
|
104
|
+
// Image base is oven/bun:1-alpine — busybox wget is available.
|
|
105
|
+
lines.push(" healthcheck:");
|
|
106
|
+
lines.push(' test: ["CMD", "wget", "-qO-", "http://127.0.0.1:5005/health"]');
|
|
107
|
+
lines.push(" interval: 30s");
|
|
108
|
+
lines.push(" timeout: 5s");
|
|
109
|
+
lines.push(" retries: 3");
|
|
110
|
+
lines.push(" start_period: 20s");
|
|
84
111
|
if (usePostgres) {
|
|
85
112
|
lines.push(" depends_on:");
|
|
86
113
|
lines.push(` arc-db-${name}:`);
|
|
@@ -149,6 +176,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
149
176
|
lines.push(` image: ${image}`);
|
|
150
177
|
lines.push(` container_name: arc-db-${name}`);
|
|
151
178
|
lines.push(" restart: unless-stopped");
|
|
179
|
+
pushLogging(lines);
|
|
152
180
|
lines.push(" environment:");
|
|
153
181
|
lines.push(" POSTGRES_USER: arc");
|
|
154
182
|
lines.push(" POSTGRES_DB: arc");
|
|
@@ -186,11 +214,17 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
186
214
|
lines.push(" image: otel/opentelemetry-collector-contrib:0.114.0");
|
|
187
215
|
lines.push(" container_name: arc-otel-collector");
|
|
188
216
|
lines.push(" restart: unless-stopped");
|
|
217
|
+
pushLogging(lines);
|
|
218
|
+
// Root needed for the hostmetrics (/hostfs) + docker_stats (docker.sock)
|
|
219
|
+
// receivers — the image's default user 10001 can read neither.
|
|
220
|
+
lines.push(" user: \"0:0\"");
|
|
189
221
|
lines.push(" command: [\"--config=/etc/otelcol-contrib/config.yaml\"]");
|
|
190
222
|
lines.push(" volumes:");
|
|
191
223
|
lines.push(
|
|
192
224
|
" - ./observability/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro",
|
|
193
225
|
);
|
|
226
|
+
lines.push(" - /:/hostfs:ro # hostmetrics root_path");
|
|
227
|
+
lines.push(" - /var/run/docker.sock:/var/run/docker.sock:ro # docker_stats");
|
|
194
228
|
lines.push(" networks: [arc-net]");
|
|
195
229
|
lines.push(" expose:");
|
|
196
230
|
lines.push(" - \"4317\" # OTLP gRPC");
|
|
@@ -206,6 +240,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
206
240
|
lines.push(" image: grafana/tempo:2.6.1");
|
|
207
241
|
lines.push(" container_name: arc-tempo");
|
|
208
242
|
lines.push(" restart: unless-stopped");
|
|
243
|
+
pushLogging(lines);
|
|
209
244
|
lines.push(" command: [\"-config.file=/etc/tempo.yaml\"]");
|
|
210
245
|
lines.push(" user: \"0\" # tempo writes to /var/tempo, owned by root in the image");
|
|
211
246
|
lines.push(" volumes:");
|
|
@@ -221,6 +256,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
221
256
|
lines.push(" image: grafana/loki:3.3.2");
|
|
222
257
|
lines.push(" container_name: arc-loki");
|
|
223
258
|
lines.push(" restart: unless-stopped");
|
|
259
|
+
pushLogging(lines);
|
|
224
260
|
lines.push(" command: [\"-config.file=/etc/loki/local-config.yaml\"]");
|
|
225
261
|
lines.push(" user: \"0\"");
|
|
226
262
|
lines.push(" volumes:");
|
|
@@ -236,6 +272,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
236
272
|
lines.push(" image: prom/prometheus:v2.55.1");
|
|
237
273
|
lines.push(" container_name: arc-prometheus");
|
|
238
274
|
lines.push(" restart: unless-stopped");
|
|
275
|
+
pushLogging(lines);
|
|
239
276
|
lines.push(" command:");
|
|
240
277
|
lines.push(" - \"--config.file=/etc/prometheus/prometheus.yml\"");
|
|
241
278
|
lines.push(" - \"--storage.tsdb.path=/prometheus\"");
|
|
@@ -250,11 +287,38 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
250
287
|
lines.push(" - \"9090\" # HTTP API + remote_write receiver");
|
|
251
288
|
lines.push("");
|
|
252
289
|
|
|
290
|
+
// Grafana Alloy — tails stdout/stderr of every container (Docker API)
|
|
291
|
+
// and pushes to Loki. Complements in-app OTLP logs with infra-container
|
|
292
|
+
// output and app crash logs.
|
|
293
|
+
lines.push(" alloy:");
|
|
294
|
+
lines.push(" image: grafana/alloy:v1.16.1");
|
|
295
|
+
lines.push(" container_name: arc-alloy");
|
|
296
|
+
lines.push(" restart: unless-stopped");
|
|
297
|
+
pushLogging(lines);
|
|
298
|
+
lines.push(" user: \"0\" # docker.sock access");
|
|
299
|
+
lines.push(" command:");
|
|
300
|
+
lines.push(" - run");
|
|
301
|
+
lines.push(" - --server.http.listen-addr=0.0.0.0:12345");
|
|
302
|
+
lines.push(" - --storage.path=/var/lib/alloy/data");
|
|
303
|
+
lines.push(" - /etc/alloy/config.alloy");
|
|
304
|
+
lines.push(" volumes:");
|
|
305
|
+
lines.push(" - ./observability/alloy-config.alloy:/etc/alloy/config.alloy:ro");
|
|
306
|
+
lines.push(" - /var/run/docker.sock:/var/run/docker.sock:ro");
|
|
307
|
+
lines.push(" - alloy_data:/var/lib/alloy/data");
|
|
308
|
+
lines.push(" networks: [arc-net]");
|
|
309
|
+
lines.push(" expose:");
|
|
310
|
+
lines.push(" - \"12345\" # Alloy self-metrics (Prom scrape)");
|
|
311
|
+
lines.push(" depends_on:");
|
|
312
|
+
lines.push(" - loki");
|
|
313
|
+
lines.push("");
|
|
314
|
+
|
|
253
315
|
const adminPasswordEnv = cfg.observability.adminPasswordEnv ?? "ARC_OBSERVABILITY_PASSWORD";
|
|
316
|
+
const grafanaDomain = observabilityDomain(cfg);
|
|
254
317
|
lines.push(" grafana:");
|
|
255
318
|
lines.push(" image: grafana/grafana:11.4.0");
|
|
256
319
|
lines.push(" container_name: arc-grafana");
|
|
257
320
|
lines.push(" restart: unless-stopped");
|
|
321
|
+
pushLogging(lines);
|
|
258
322
|
lines.push(" environment:");
|
|
259
323
|
lines.push(" GF_SECURITY_ADMIN_USER: admin");
|
|
260
324
|
lines.push(
|
|
@@ -262,6 +326,11 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
262
326
|
);
|
|
263
327
|
lines.push(" GF_USERS_ALLOW_SIGN_UP: \"false\"");
|
|
264
328
|
lines.push(" GF_AUTH_ANONYMOUS_ENABLED: \"false\"");
|
|
329
|
+
if (grafanaDomain) {
|
|
330
|
+
// Correct absolute links in alert notifications + UI redirects behind
|
|
331
|
+
// the Caddy reverse proxy.
|
|
332
|
+
lines.push(` GF_SERVER_ROOT_URL: "https://${grafanaDomain}"`);
|
|
333
|
+
}
|
|
265
334
|
// Subpath-less — the upstream URL is served from root via Caddy reverse
|
|
266
335
|
// proxy on a dedicated subdomain.
|
|
267
336
|
lines.push(" volumes:");
|
|
@@ -278,6 +347,9 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
278
347
|
lines.push(
|
|
279
348
|
" - ./observability/grafana-dashboards:/etc/grafana/provisioning/dashboards/arc:ro",
|
|
280
349
|
);
|
|
350
|
+
lines.push(
|
|
351
|
+
" - ./observability/grafana-alerting:/etc/grafana/provisioning/alerting:ro",
|
|
352
|
+
);
|
|
281
353
|
lines.push(" - grafana_data:/var/lib/grafana");
|
|
282
354
|
lines.push(" networks: [arc-net]");
|
|
283
355
|
lines.push(" expose:");
|
|
@@ -308,6 +380,7 @@ export function generateCompose({ cfg }: ComposeOptions): string {
|
|
|
308
380
|
lines.push(" loki_data:");
|
|
309
381
|
lines.push(" prometheus_data:");
|
|
310
382
|
lines.push(" grafana_data:");
|
|
383
|
+
lines.push(" alloy_data:");
|
|
311
384
|
}
|
|
312
385
|
|
|
313
386
|
return lines.join("\n") + "\n";
|
package/src/deploy/config.ts
CHANGED
|
@@ -109,6 +109,11 @@ export interface DeployObservability {
|
|
|
109
109
|
logs?: string;
|
|
110
110
|
metrics?: string;
|
|
111
111
|
};
|
|
112
|
+
/** Webhook URL for Grafana alert notifications (Slack/Discord/anything that
|
|
113
|
+
* accepts a POST). When absent, alert rules are still provisioned but no
|
|
114
|
+
* contact point / notification policy is created — alerts are visible in
|
|
115
|
+
* the Grafana UI only. */
|
|
116
|
+
alertWebhookUrl?: string;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
export interface DeployRegistry {
|
|
@@ -344,6 +349,15 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
344
349
|
metrics: optionalString(retentionRaw, "observability.retention.metrics"),
|
|
345
350
|
};
|
|
346
351
|
}
|
|
352
|
+
const alertWebhookUrl = optionalString(
|
|
353
|
+
observabilityRaw,
|
|
354
|
+
"observability.alertWebhookUrl",
|
|
355
|
+
);
|
|
356
|
+
if (alertWebhookUrl !== undefined && !/^https?:\/\/.+/.test(alertWebhookUrl)) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`deploy.arc.json: observability.alertWebhookUrl must be an http(s) URL (got "${alertWebhookUrl}")`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
347
361
|
validated.observability = {
|
|
348
362
|
enabled: enabledRaw,
|
|
349
363
|
subdomain: optionalString(observabilityRaw, "observability.subdomain") ?? "observability",
|
|
@@ -351,6 +365,7 @@ export function validateDeployConfig(input: unknown): DeployConfig {
|
|
|
351
365
|
optionalString(observabilityRaw, "observability.adminPasswordEnv") ??
|
|
352
366
|
"ARC_OBSERVABILITY_PASSWORD",
|
|
353
367
|
retention,
|
|
368
|
+
alertWebhookUrl,
|
|
354
369
|
};
|
|
355
370
|
}
|
|
356
371
|
|