deepdebug-local-agent 1.0.23 → 1.1.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/.claude/context.md +76 -0
- package/.claude/skills/README.md +25 -0
- package/.claude/skills/tdd-debugging/SKILL.md +53 -0
- package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
- package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
- package/.claude/standards/README.md +51 -0
- package/.claude/standards/clean-code.md +75 -0
- package/.claude/standards/code-review-checklist.md +61 -0
- package/.claude/standards/layered-architecture.md +163 -0
- package/.claude/standards/node.md +45 -0
- package/.claude/standards/security-owasp.md +114 -0
- package/.claude/standards/simplicity.md +42 -0
- package/.claude/standards/solid.md +64 -0
- package/.claude/standards/testing.md +59 -0
- package/.cursor/rules/standards.mdc +17 -0
- package/.github/workflows/build-byoc-image.yml +315 -0
- package/.github/workflows/deploy-gcp.yml +102 -0
- package/.github/workflows/npm-publish.yml +69 -8
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +69 -0
- package/CLAUDE.md +13 -0
- package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
- package/Dockerfile +4 -0
- package/Dockerfile.full +55 -3
- package/bin/install.js +16 -5
- package/cloudbuild-full.yaml +57 -0
- package/docker-compose.yml +5 -3
- package/docs/OBSERVABILITY.md +164 -0
- package/docs/byoc-agent-image.md +70 -0
- package/docs/graphify-packaging-plan.md +133 -0
- package/docs/graphify-validation-run-2026-10-28.md +243 -0
- package/docs/releasing.md +43 -0
- package/docs/signed-commands.md +163 -0
- package/env.example +30 -0
- package/helm/Chart.yaml +2 -2
- package/helm/templates/deployment.yaml +13 -3
- package/helm/templates/networkpolicy.yaml +22 -0
- package/helm/templates/secret.yaml +1 -0
- package/helm/values.yaml +13 -14
- package/mcp-server.js +4 -1
- package/package.json +9 -4
- package/scripts/upgrade-observability-push.sh +227 -0
- package/src/ai-engine.js +22 -22
- package/src/config/trusted-signing-keys.json +3 -0
- package/src/detectors/build-tool-detector.js +50 -0
- package/src/detectors/multi-project-detector.js +22 -22
- package/src/detectors/service-detector.js +24 -24
- package/src/exec-utils.js +26 -4
- package/src/git/base-git-provider.js +117 -117
- package/src/git/bitbucket-provider.js +2 -2
- package/src/git/git-provider-registry.js +18 -18
- package/src/git/github-provider.js +13 -13
- package/src/git/multi-repo-pr.js +161 -0
- package/src/mcp-http-server.js +32 -16
- package/src/observability/bundle-files.js +418 -0
- package/src/observability/detect.js +151 -0
- package/src/observability/provision.js +212 -0
- package/src/observability/render-cli.js +59 -0
- package/src/observability/routes.js +88 -0
- package/src/patch/patch-engine.js +28 -28
- package/src/patch-manager.js +85 -85
- package/src/patch_bkp.js +14 -14
- package/src/ports.js +4 -4
- package/src/routes/workspace.route.js +47 -16
- package/src/runtimes/base-runtime.js +75 -75
- package/src/runtimes/java/gradle-runtime.js +17 -17
- package/src/runtimes/java/java-integrations.js +25 -25
- package/src/runtimes/java/maven-runtime.js +34 -34
- package/src/runtimes/node/node-integrations.js +1 -1
- package/src/runtimes/node/npm-runtime.js +14 -14
- package/src/runtimes/node/yarn-runtime.js +4 -4
- package/src/runtimes/runtime-registry.js +25 -25
- package/src/server-local.js +4 -4
- package/src/server.js +1366 -333
- package/src/siblings/burst-suppressor.js +89 -0
- package/src/siblings/capabilities.js +273 -0
- package/src/siblings/env-compat.cjs +58 -0
- package/src/siblings/incident-forwarder.js +320 -0
- package/src/siblings/logger.js +15 -0
- package/src/siblings/metrics.js +94 -0
- package/src/utils/agent-auth.js +184 -0
- package/src/utils/approval-token.js +56 -0
- package/src/utils/bind-host.js +29 -0
- package/src/utils/branch-refspec.js +98 -0
- package/src/utils/child-env.js +34 -0
- package/src/utils/environment-diagnostics.js +55 -55
- package/src/utils/exec-policy.js +962 -0
- package/src/utils/exec-utils.js +15 -15
- package/src/utils/file-count.js +46 -0
- package/src/utils/fs-utils.js +10 -10
- package/src/utils/multi-repo-open.js +132 -0
- package/src/utils/open-gate.js +183 -0
- package/src/utils/ranged-read.js +65 -0
- package/src/utils/request-guards.js +77 -0
- package/src/utils/request-signing.js +342 -0
- package/src/utils/root-recovery.js +244 -0
- package/src/utils/safe-exec-args.js +58 -0
- package/src/utils/sandbox.js +182 -0
- package/src/utils/search-outcome.js +56 -0
- package/src/utils/workspace-root.js +80 -0
- package/src/validators/syntax-validator.js +149 -0
- package/src/workspace/detect-port.js +16 -16
- package/src/workspace/file-reader.js +1 -1
- package/src/workspace/process-manager.js +46 -46
- package/src/workspace-manager.js +86 -24
- package/test/adapters/registry.test.js +9 -2
- package/test/agent-auth.test.js +194 -0
- package/test/bind-host.test.js +26 -0
- package/test/branch-refspec.test.js +224 -0
- package/test/burst-suppressor.test.js +60 -0
- package/test/capabilities-access.test.js +47 -0
- package/test/capabilities.test.js +87 -0
- package/test/child-env.test.js +23 -0
- package/test/env-compat.test.js +71 -0
- package/test/exec-policy.test.js +463 -0
- package/test/file-count.test.js +64 -0
- package/test/incident-forwarder.test.js +231 -0
- package/test/multi-repo-open.test.js +180 -0
- package/test/multi-repo-pr.test.js +129 -0
- package/test/observability-detect.test.js +199 -0
- package/test/open-gate.test.js +341 -0
- package/test/ranged-read.test.js +60 -0
- package/test/request-guards.test.js +91 -0
- package/test/request-signing.test.js +284 -0
- package/test/root-recovery-sidecar.test.js +171 -0
- package/test/root-recovery.test.js +274 -0
- package/test/safe-exec-args.test.js +58 -0
- package/test/sandbox.test.js +98 -0
- package/test/search-outcome.test.js +50 -0
- package/test/workspace-root.test.js +64 -0
- package/tools/graphify-validation/README.md +92 -0
- package/tools/graphify-validation/config/questions.json +31 -0
- package/tools/graphify-validation/config/repos.json +9 -0
- package/tools/graphify-validation/config/symbols.json +6 -0
- package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
- package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
- package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
- package/tools/graphify-validation/lib/graph_metrics.py +96 -0
- package/tools/graphify-validation/lib/gv_common.py +130 -0
- package/tools/graphify-validation/lib/licenses.py +47 -0
- package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
- package/tools/graphify-validation/lib/recall.py +115 -0
- package/tools/graphify-validation/lib/report.py +226 -0
- package/tools/graphify-validation/lib/run_all.py +237 -0
- package/tools/graphify-validation/lib/run_graphify.py +144 -0
- package/tools/graphify-validation/lib/rusage_helper.py +17 -0
- package/tools/graphify-validation/lib/sample_edges.py +246 -0
- package/tools/graphify-validation/lib/spring_fixture.py +81 -0
- package/tools/graphify-validation/requirements-py311.lock +508 -0
- package/tools/graphify-validation/requirements-tools.in +1 -0
- package/tools/graphify-validation/requirements-tools.lock +371 -0
- package/tools/graphify-validation/requirements.in +1 -0
- package/tools/graphify-validation/requirements.lock +502 -0
- package/tools/graphify-validation/run.sh +43 -0
- package/tools/graphify-validation/tests/test_harness.py +151 -0
- package/cloudbuild-agent-qa.yaml +0 -43
- package/cloudbuild-agent-staging.yaml +0 -40
- package/cloudbuild.yaml.deprecated +0 -42
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observability/bundle-files.js
|
|
3
|
+
*
|
|
4
|
+
* The client-side observability bundle, embedded as templated strings so it
|
|
5
|
+
* ships inside the pkg-bundled agent binary (no loose asset files to lose).
|
|
6
|
+
*
|
|
7
|
+
* This is the CLIENT delivery of the same stack we run for ourselves in
|
|
8
|
+
* `vh-observability` (Grafana + Loki + Alloy + Alertmanager). The difference is
|
|
9
|
+
* only in how logs get in and where chunks live:
|
|
10
|
+
*
|
|
11
|
+
* vh-observability (ours, Cloud Run): logs = Cloud Logging -> Pub/Sub (gcplog)
|
|
12
|
+
* storage = GCS
|
|
13
|
+
* client bundle (this, VPS): logs = docker.sock (every container)
|
|
14
|
+
* storage = local filesystem volume
|
|
15
|
+
*
|
|
16
|
+
* Everything downstream is identical: Loki stores + rules-evaluates, Alertmanager
|
|
17
|
+
* posts firing alerts to the gateway webhook (tenant resolved from the ?s= trigger
|
|
18
|
+
* secret, exactly like ours), and Grafana serves a logs dashboard whose "Open in
|
|
19
|
+
* Workbench" data link hits /api/v1/webhooks/from-log.
|
|
20
|
+
*
|
|
21
|
+
* Placeholders replaced by renderBundle():
|
|
22
|
+
* __GATEWAY_URL__ e.g. https://api.visionhammer.io (no trailing slash)
|
|
23
|
+
* __TRIGGER_SECRET__ the tenant's vht_... alertmanager trigger secret
|
|
24
|
+
* __ENVIRONMENT__ static env label for this client's logs (e.g. production)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ---- Loki: filesystem backend (no GCS); rules + ruler -> Alertmanager ----
|
|
28
|
+
const LOKI_CONFIG = `# DeepDebug client observability - Loki (docker-compose, filesystem backend).
|
|
29
|
+
auth_enabled: false
|
|
30
|
+
|
|
31
|
+
server:
|
|
32
|
+
http_listen_port: 3100
|
|
33
|
+
grpc_listen_port: 9095
|
|
34
|
+
log_level: warn
|
|
35
|
+
|
|
36
|
+
common:
|
|
37
|
+
instance_addr: 127.0.0.1
|
|
38
|
+
path_prefix: /loki
|
|
39
|
+
replication_factor: 1
|
|
40
|
+
ring:
|
|
41
|
+
kvstore:
|
|
42
|
+
store: inmemory
|
|
43
|
+
storage:
|
|
44
|
+
filesystem:
|
|
45
|
+
chunks_directory: /loki/chunks
|
|
46
|
+
rules_directory: /loki/rules-store
|
|
47
|
+
|
|
48
|
+
schema_config:
|
|
49
|
+
configs:
|
|
50
|
+
- from: 2024-01-01
|
|
51
|
+
store: tsdb
|
|
52
|
+
object_store: filesystem
|
|
53
|
+
schema: v13
|
|
54
|
+
index:
|
|
55
|
+
prefix: index_
|
|
56
|
+
period: 24h
|
|
57
|
+
|
|
58
|
+
limits_config:
|
|
59
|
+
retention_period: 336h # 14 days
|
|
60
|
+
reject_old_samples: true
|
|
61
|
+
# Wide enough to accept a host's existing docker log backlog on first start.
|
|
62
|
+
# loki.source.docker replays each container's full history oldest-first; a
|
|
63
|
+
# tight window (e.g. 168h) 400-rejects that backlog and queues fresh lines
|
|
64
|
+
# behind it. 720h clears the backlog cleanly; retention above still caps disk.
|
|
65
|
+
reject_old_samples_max_age: 720h
|
|
66
|
+
volume_enabled: true
|
|
67
|
+
allow_structured_metadata: true
|
|
68
|
+
|
|
69
|
+
compactor:
|
|
70
|
+
working_directory: /loki/compactor
|
|
71
|
+
retention_enabled: true
|
|
72
|
+
delete_request_store: filesystem
|
|
73
|
+
|
|
74
|
+
# Ruler evaluates the LogQL error rule and pushes firing alerts to Alertmanager.
|
|
75
|
+
ruler:
|
|
76
|
+
storage:
|
|
77
|
+
type: local
|
|
78
|
+
local:
|
|
79
|
+
directory: /etc/loki/rules
|
|
80
|
+
rule_path: /loki/rules-tmp
|
|
81
|
+
alertmanager_url: http://alertmanager:9093
|
|
82
|
+
# Alertmanager v0.27 removed the legacy v1 push API; Loki's ruler defaults to
|
|
83
|
+
# POSTing /api/v1/alerts, which that Alertmanager rejects, so alerts silently
|
|
84
|
+
# never arrive. Force the v2 endpoint.
|
|
85
|
+
enable_alertmanager_v2: true
|
|
86
|
+
enable_api: true
|
|
87
|
+
ring:
|
|
88
|
+
kvstore:
|
|
89
|
+
store: inmemory
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
// ---- Loki rule: error rate by service -> critical alert ----
|
|
93
|
+
const LOKI_RULE = `# DeepDebug client error alerts. Labels service/severity/environment flow into
|
|
94
|
+
# the gateway incident (AlertmanagerIngestService reads them).
|
|
95
|
+
groups:
|
|
96
|
+
- name: client-errors
|
|
97
|
+
rules:
|
|
98
|
+
- alert: ClientServiceErrors
|
|
99
|
+
expr: |
|
|
100
|
+
sum by (service, environment) (rate({job="docker", service!~"deepdebug-observability-.*|vh-obs-.*|visionhammer-agent"} |~ \`(?i)\\bERROR\\b\` [5m])) > 0
|
|
101
|
+
for: 2m
|
|
102
|
+
labels:
|
|
103
|
+
severity: critical
|
|
104
|
+
origin: deepdebug-observability
|
|
105
|
+
annotations:
|
|
106
|
+
summary: "errors on {{ $labels.service }} ({{ $labels.environment }})"
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
// ---- Alloy: tail every container via docker.sock, label by container name ----
|
|
110
|
+
const ALLOY_CONFIG = `// DeepDebug client observability - Alloy.
|
|
111
|
+
// Tails every Docker container on the host (docker.sock) and ships to Loki.
|
|
112
|
+
// The same stream is also pushed to VisionHammer (OTLP/HTTP JSON, outbound only)
|
|
113
|
+
// so Live logs and the services list see this server; auth is the tenant's
|
|
114
|
+
// trigger secret, the same one the Alertmanager below uses.
|
|
115
|
+
// service = container name; environment = the static label this client was
|
|
116
|
+
// provisioned with; level is parsed from the line for quick filtering.
|
|
117
|
+
|
|
118
|
+
discovery.docker "containers" {
|
|
119
|
+
host = "unix:///var/run/docker.sock"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
discovery.relabel "containers" {
|
|
123
|
+
targets = discovery.docker.containers.targets
|
|
124
|
+
|
|
125
|
+
rule {
|
|
126
|
+
source_labels = ["__meta_docker_container_name"]
|
|
127
|
+
regex = "/?(.*)"
|
|
128
|
+
target_label = "service"
|
|
129
|
+
}
|
|
130
|
+
rule {
|
|
131
|
+
source_labels = ["__meta_docker_container_label_com_docker_compose_project"]
|
|
132
|
+
target_label = "project"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
loki.source.docker "default" {
|
|
137
|
+
host = "unix:///var/run/docker.sock"
|
|
138
|
+
targets = discovery.relabel.containers.output
|
|
139
|
+
labels = { job = "docker" }
|
|
140
|
+
forward_to = [loki.process.default.receiver]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
loki.process "default" {
|
|
144
|
+
forward_to = [loki.write.default.receiver, otelcol.receiver.loki.visionhammer.receiver]
|
|
145
|
+
|
|
146
|
+
stage.regex {
|
|
147
|
+
expression = "(?i)(?P<level>ERROR|WARNING|WARN|INFO|DEBUG|SEVERE|FATAL|CRITICAL)"
|
|
148
|
+
}
|
|
149
|
+
stage.labels {
|
|
150
|
+
values = { level = "" }
|
|
151
|
+
}
|
|
152
|
+
stage.static_labels {
|
|
153
|
+
values = { environment = "__ENVIRONMENT__" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
loki.write "default" {
|
|
158
|
+
endpoint {
|
|
159
|
+
url = "http://loki:3100/loki/api/v1/push"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- push to VisionHammer: Loki entries -> OTLP logs (labels become attributes) ----
|
|
164
|
+
otelcol.receiver.loki "visionhammer" {
|
|
165
|
+
output {
|
|
166
|
+
logs = [otelcol.processor.batch.visionhammer.input]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
otelcol.processor.batch "visionhammer" {
|
|
171
|
+
send_batch_size = 500
|
|
172
|
+
send_batch_max_size = 2000
|
|
173
|
+
timeout = "5s"
|
|
174
|
+
output {
|
|
175
|
+
logs = [otelcol.exporter.otlphttp.visionhammer.input]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
otelcol.exporter.otlphttp "visionhammer" {
|
|
180
|
+
client {
|
|
181
|
+
endpoint = "__GATEWAY_URL__"
|
|
182
|
+
compression = "gzip"
|
|
183
|
+
headers = { "Authorization" = "Bearer __TRIGGER_SECRET__" }
|
|
184
|
+
}
|
|
185
|
+
encoding = "json"
|
|
186
|
+
logs_endpoint = "__GATEWAY_URL__/api/v1/webhooks/logs/otlp"
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
// ---- Alertmanager: firing alerts -> gateway webhook (tenant from ?s=) ----
|
|
191
|
+
const ALERTMANAGER_CONFIG = `route:
|
|
192
|
+
receiver: deepdebug-gateway
|
|
193
|
+
group_by: ['service']
|
|
194
|
+
group_wait: 30s
|
|
195
|
+
group_interval: 5m
|
|
196
|
+
repeat_interval: 4h
|
|
197
|
+
|
|
198
|
+
receivers:
|
|
199
|
+
- name: deepdebug-gateway
|
|
200
|
+
webhook_configs:
|
|
201
|
+
- url: '__GATEWAY_URL__/api/v1/webhooks/alertmanager'
|
|
202
|
+
send_resolved: false
|
|
203
|
+
http_config:
|
|
204
|
+
authorization:
|
|
205
|
+
type: Bearer
|
|
206
|
+
credentials: '__TRIGGER_SECRET__'
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
// ---- Grafana provisioning: Loki datasource + dashboard provider ----
|
|
210
|
+
const GRAFANA_DATASOURCE = `apiVersion: 1
|
|
211
|
+
datasources:
|
|
212
|
+
- name: Loki
|
|
213
|
+
uid: ddo-loki
|
|
214
|
+
type: loki
|
|
215
|
+
access: proxy
|
|
216
|
+
url: http://loki:3100
|
|
217
|
+
isDefault: true
|
|
218
|
+
jsonData:
|
|
219
|
+
maxLines: 1000
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
const GRAFANA_DASH_PROVIDER = `apiVersion: 1
|
|
223
|
+
providers:
|
|
224
|
+
- name: deepdebug
|
|
225
|
+
type: file
|
|
226
|
+
disableDeletion: false
|
|
227
|
+
updateIntervalSeconds: 30
|
|
228
|
+
options:
|
|
229
|
+
path: /etc/grafana/dashboards
|
|
230
|
+
foldersFromFilesStructure: false
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
// ---- Grafana dashboard: logs + error stats, "Open in Workbench" data link ----
|
|
234
|
+
const DASHBOARD = JSON.stringify({
|
|
235
|
+
uid: 'ddo-logs',
|
|
236
|
+
title: 'DeepDebug - Observability',
|
|
237
|
+
editable: true,
|
|
238
|
+
schemaVersion: 39,
|
|
239
|
+
style: 'dark',
|
|
240
|
+
tags: ['deepdebug', 'logs'],
|
|
241
|
+
time: { from: 'now-1h', to: 'now' },
|
|
242
|
+
refresh: '30s',
|
|
243
|
+
templating: {
|
|
244
|
+
list: [
|
|
245
|
+
{ name: 'service', label: 'Service', type: 'query',
|
|
246
|
+
datasource: { type: 'loki', uid: 'ddo-loki' },
|
|
247
|
+
query: 'label_values(service)', refresh: 2, multi: true, includeAll: true,
|
|
248
|
+
current: { text: 'All', value: '$__all' } },
|
|
249
|
+
{ name: 'search', label: 'Search', type: 'textbox', current: { text: '', value: '' } },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
panels: [
|
|
253
|
+
{ id: 1, title: 'Errors (last 5m)', type: 'stat',
|
|
254
|
+
datasource: { type: 'loki', uid: 'ddo-loki' },
|
|
255
|
+
gridPos: { h: 5, w: 6, x: 0, y: 0 },
|
|
256
|
+
options: { colorMode: 'background', graphMode: 'area', reduceOptions: { calcs: ['lastNotNull'] } },
|
|
257
|
+
fieldConfig: { defaults: { color: { mode: 'thresholds' },
|
|
258
|
+
thresholds: { steps: [ { color: 'green', value: null }, { color: 'orange', value: 1 }, { color: 'red', value: 10 } ] } } },
|
|
259
|
+
targets: [ { refId: 'A', expr: 'sum(count_over_time({service=~"$service"} |~ `(?i)error` [5m]))' } ] },
|
|
260
|
+
{ id: 2, title: 'Warnings (last 5m)', type: 'stat',
|
|
261
|
+
datasource: { type: 'loki', uid: 'ddo-loki' },
|
|
262
|
+
gridPos: { h: 5, w: 6, x: 6, y: 0 },
|
|
263
|
+
options: { colorMode: 'background', graphMode: 'area', reduceOptions: { calcs: ['lastNotNull'] } },
|
|
264
|
+
fieldConfig: { defaults: { color: { mode: 'thresholds' },
|
|
265
|
+
thresholds: { steps: [ { color: 'green', value: null }, { color: 'yellow', value: 1 } ] } } },
|
|
266
|
+
targets: [ { refId: 'A', expr: 'sum(count_over_time({service=~"$service"} |~ `(?i)warn` [5m]))' } ] },
|
|
267
|
+
{ id: 3, title: 'Error rate by service', type: 'timeseries',
|
|
268
|
+
datasource: { type: 'loki', uid: 'ddo-loki' },
|
|
269
|
+
gridPos: { h: 5, w: 12, x: 12, y: 0 },
|
|
270
|
+
fieldConfig: { defaults: { custom: { drawStyle: 'line', fillOpacity: 20, showPoints: 'never' }, color: { mode: 'palette-classic' } } },
|
|
271
|
+
targets: [ { refId: 'A', legendFormat: '{{service}}', expr: 'sum by (service) (rate({service=~"$service"} |~ `(?i)error` [5m]))' } ] },
|
|
272
|
+
{ id: 4, title: 'Logs', type: 'logs',
|
|
273
|
+
datasource: { type: 'loki', uid: 'ddo-loki' },
|
|
274
|
+
gridPos: { h: 12, w: 24, x: 0, y: 5 },
|
|
275
|
+
options: { showTime: true, wrapLogMessage: true, enableLogDetails: true, dedupStrategy: 'none', sortOrder: 'Descending' },
|
|
276
|
+
targets: [ { refId: 'A', expr: '{service=~"$service"} |~ `(?i)$search`' } ],
|
|
277
|
+
fieldConfig: { defaults: { links: [ {
|
|
278
|
+
title: 'Open in DeepDebug Workbench',
|
|
279
|
+
url: '__GATEWAY_URL__/api/v1/webhooks/from-log?s=__TRIGGER_SECRET__&service=${service}&environment=__ENVIRONMENT__&severity=critical&message=${__value.raw}',
|
|
280
|
+
targetBlank: true,
|
|
281
|
+
} ] } } },
|
|
282
|
+
],
|
|
283
|
+
}, null, 2);
|
|
284
|
+
|
|
285
|
+
// ---- docker-compose: the four containers, Grafana bound to loopback only ----
|
|
286
|
+
const DOCKER_COMPOSE = `name: deepdebug-observability
|
|
287
|
+
services:
|
|
288
|
+
loki:
|
|
289
|
+
image: grafana/loki:3.1.1
|
|
290
|
+
command: -config.file=/etc/loki/loki-config.yaml
|
|
291
|
+
volumes:
|
|
292
|
+
- ./loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
|
|
293
|
+
- ./loki/rules:/etc/loki/rules:ro
|
|
294
|
+
- ddo-loki:/loki
|
|
295
|
+
restart: unless-stopped
|
|
296
|
+
|
|
297
|
+
alloy:
|
|
298
|
+
image: grafana/alloy:v1.3.1
|
|
299
|
+
command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
|
|
300
|
+
volumes:
|
|
301
|
+
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
|
|
302
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
303
|
+
- ddo-alloy:/var/lib/alloy/data
|
|
304
|
+
depends_on:
|
|
305
|
+
- loki
|
|
306
|
+
restart: unless-stopped
|
|
307
|
+
|
|
308
|
+
grafana:
|
|
309
|
+
image: grafana/grafana:11.1.0
|
|
310
|
+
environment:
|
|
311
|
+
- GF_SECURITY_ADMIN_USER=\${GF_ADMIN_USER:-admin}
|
|
312
|
+
- GF_SECURITY_ADMIN_PASSWORD=\${GF_ADMIN_PASSWORD:-deepdebug}
|
|
313
|
+
- GF_USERS_ALLOW_SIGN_UP=false
|
|
314
|
+
- GF_USERS_DEFAULT_THEME=dark
|
|
315
|
+
- GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/dashboards/ddo-logs.json
|
|
316
|
+
ports:
|
|
317
|
+
# Loopback only: reach it over an SSH tunnel, never expose to the internet.
|
|
318
|
+
- "127.0.0.1:3000:3000"
|
|
319
|
+
volumes:
|
|
320
|
+
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
|
321
|
+
- ./grafana/dashboards:/etc/grafana/dashboards:ro
|
|
322
|
+
- ddo-grafana:/var/lib/grafana
|
|
323
|
+
depends_on:
|
|
324
|
+
- loki
|
|
325
|
+
restart: unless-stopped
|
|
326
|
+
|
|
327
|
+
alertmanager:
|
|
328
|
+
image: prom/alertmanager:v0.27.0
|
|
329
|
+
command:
|
|
330
|
+
- --config.file=/etc/alertmanager/alertmanager.yml
|
|
331
|
+
- --storage.path=/tmp/alertmanager
|
|
332
|
+
volumes:
|
|
333
|
+
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
|
334
|
+
restart: unless-stopped
|
|
335
|
+
|
|
336
|
+
volumes:
|
|
337
|
+
ddo-loki:
|
|
338
|
+
ddo-alloy:
|
|
339
|
+
ddo-grafana:
|
|
340
|
+
`;
|
|
341
|
+
|
|
342
|
+
// ---- Tunnel overlay (opt-in): expose Loki to the gateway, bearer-gated ----
|
|
343
|
+
// Caddy sits in front of Loki and 403s anything without the exact bearer token,
|
|
344
|
+
// so the Cloudflare quick tunnel never exposes an unauthenticated Loki. The
|
|
345
|
+
// gateway's loki-logging config stores this same token.
|
|
346
|
+
const CADDY_CONFIG = `# DeepDebug observability - bearer-auth gate in front of Loki.
|
|
347
|
+
:8080 {
|
|
348
|
+
@unauthorized not header Authorization "Bearer __LOKI_TUNNEL_TOKEN__"
|
|
349
|
+
respond @unauthorized "forbidden" 403
|
|
350
|
+
reverse_proxy loki:3100
|
|
351
|
+
}
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
// Opt-in overlay compose. Start alongside the base file:
|
|
355
|
+
// docker compose -f docker-compose.yml -f docker-compose.tunnel.yml up -d
|
|
356
|
+
// then read the public URL:
|
|
357
|
+
// docker compose logs cloudflared | grep -o 'https://[^ ]*trycloudflare.com'
|
|
358
|
+
const TUNNEL_COMPOSE = `name: deepdebug-observability
|
|
359
|
+
services:
|
|
360
|
+
loki-auth:
|
|
361
|
+
image: caddy:2.8
|
|
362
|
+
volumes:
|
|
363
|
+
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
364
|
+
depends_on:
|
|
365
|
+
- loki
|
|
366
|
+
restart: unless-stopped
|
|
367
|
+
|
|
368
|
+
cloudflared:
|
|
369
|
+
image: cloudflare/cloudflared:2024.8.3
|
|
370
|
+
command: tunnel --no-autoupdate --url http://loki-auth:8080
|
|
371
|
+
depends_on:
|
|
372
|
+
- loki-auth
|
|
373
|
+
restart: unless-stopped
|
|
374
|
+
`;
|
|
375
|
+
|
|
376
|
+
/** The compose project name; also used to recognise OUR own stack in detection. */
|
|
377
|
+
export const COMPOSE_PROJECT = 'deepdebug-observability';
|
|
378
|
+
|
|
379
|
+
/** Raw templated file map (relative path -> content, placeholders unresolved). */
|
|
380
|
+
export const BUNDLE_TEMPLATES = {
|
|
381
|
+
'docker-compose.yml': DOCKER_COMPOSE,
|
|
382
|
+
'loki/loki-config.yaml': LOKI_CONFIG,
|
|
383
|
+
// Loki's local ruler reads rules from <rules_dir>/<tenant>/; with
|
|
384
|
+
// auth_enabled=false the tenant is the literal "fake". A file placed directly
|
|
385
|
+
// under rules/ (no tenant subdir) is silently ignored and no alert ever fires.
|
|
386
|
+
'loki/rules/fake/client.yaml': LOKI_RULE,
|
|
387
|
+
'alloy/config.alloy': ALLOY_CONFIG,
|
|
388
|
+
'alertmanager/alertmanager.yml': ALERTMANAGER_CONFIG,
|
|
389
|
+
'grafana/provisioning/datasources/loki.yaml': GRAFANA_DATASOURCE,
|
|
390
|
+
'grafana/provisioning/dashboards/provider.yaml': GRAFANA_DASH_PROVIDER,
|
|
391
|
+
'grafana/dashboards/ddo-logs.json': DASHBOARD,
|
|
392
|
+
// Opt-in tunnel overlay (see renderBundle lokiTunnelToken).
|
|
393
|
+
'caddy/Caddyfile': CADDY_CONFIG,
|
|
394
|
+
'docker-compose.tunnel.yml': TUNNEL_COMPOSE,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Resolve the bundle for a given client. Pure: same inputs -> same file map.
|
|
399
|
+
* @param {{gatewayUrl:string, triggerSecret:string, environment?:string,
|
|
400
|
+
* lokiTunnelToken?:string}} opts
|
|
401
|
+
* @returns {Record<string,string>} relative path -> final file content
|
|
402
|
+
*/
|
|
403
|
+
export function renderBundle({ gatewayUrl, triggerSecret, environment = 'production',
|
|
404
|
+
lokiTunnelToken = '' } = {}) {
|
|
405
|
+
if (!gatewayUrl || !triggerSecret) {
|
|
406
|
+
throw new Error('renderBundle requires gatewayUrl and triggerSecret');
|
|
407
|
+
}
|
|
408
|
+
const url = String(gatewayUrl).replace(/\/+$/, ''); // strip trailing slashes
|
|
409
|
+
const out = {};
|
|
410
|
+
for (const [path, tmpl] of Object.entries(BUNDLE_TEMPLATES)) {
|
|
411
|
+
out[path] = tmpl
|
|
412
|
+
.split('__GATEWAY_URL__').join(url)
|
|
413
|
+
.split('__TRIGGER_SECRET__').join(triggerSecret)
|
|
414
|
+
.split('__ENVIRONMENT__').join(environment)
|
|
415
|
+
.split('__LOKI_TUNNEL_TOKEN__').join(lokiTunnelToken);
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observability/detect.js
|
|
3
|
+
*
|
|
4
|
+
* "Does this client already have observability?" If not, the agent offers to
|
|
5
|
+
* provision the DeepDebug bundle. The classifier is a PURE function over the
|
|
6
|
+
* host's running containers + processes so it can be unit-tested without Docker.
|
|
7
|
+
*
|
|
8
|
+
* We only ever recommend provisioning when we find NOTHING that already ships
|
|
9
|
+
* logs/metrics somewhere. Finding a real stack (Datadog, ELK, an existing
|
|
10
|
+
* Grafana, an otel-collector, ...) means the client has observability and we
|
|
11
|
+
* must NOT bulldoze it -- we report what we found and recommend 'skip'.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { exec } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
import { COMPOSE_PROJECT } from './bundle-files.js';
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Known observability signals. Matched (case-insensitively, as substrings)
|
|
22
|
+
* against container image/name AND process command lines. Deliberately broad:
|
|
23
|
+
* a false "they have it" is safe (we skip); a false "they have nothing" is not
|
|
24
|
+
* (we would deploy on top of an existing stack).
|
|
25
|
+
*/
|
|
26
|
+
export const OBSERVABILITY_SIGNALS = [
|
|
27
|
+
// log shippers / agents
|
|
28
|
+
'promtail', 'fluent-bit', 'fluentbit', 'fluentd', 'vector', 'filebeat',
|
|
29
|
+
'logstash', 'otelcol', 'otel-collector', 'opentelemetry', 'telegraf',
|
|
30
|
+
// stores / backends
|
|
31
|
+
'loki', 'elasticsearch', 'opensearch', 'victoriametrics', 'victoria-metrics',
|
|
32
|
+
'prometheus', 'thanos', 'mimir', 'signoz', 'clickhouse-observability',
|
|
33
|
+
// UIs
|
|
34
|
+
'grafana', 'kibana',
|
|
35
|
+
// SaaS agents
|
|
36
|
+
'datadog', 'dd-agent', 'newrelic', 'new-relic', 'splunk', 'dynatrace',
|
|
37
|
+
'sematext', 'sumologic', 'sumo-logic', 'instana',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify whether the host already has observability.
|
|
42
|
+
*
|
|
43
|
+
* @param {{containers?: Array<{name?:string,image?:string,project?:string}>,
|
|
44
|
+
* processes?: string[]}} input
|
|
45
|
+
* @returns {{hasObservability:boolean, found:string[], ours:string[],
|
|
46
|
+
* recommendation:'provision'|'skip'}}
|
|
47
|
+
*/
|
|
48
|
+
export function classifyObservability({ containers = [], processes = [] } = {}) {
|
|
49
|
+
const found = new Set();
|
|
50
|
+
const ours = new Set();
|
|
51
|
+
|
|
52
|
+
const scan = (haystack, sourceLabel) => {
|
|
53
|
+
if (!haystack) return;
|
|
54
|
+
const h = String(haystack).toLowerCase();
|
|
55
|
+
for (const sig of OBSERVABILITY_SIGNALS) {
|
|
56
|
+
if (h.includes(sig)) found.add(sig);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const c of containers) {
|
|
61
|
+
// Our own stack must never count as "the client already has observability".
|
|
62
|
+
if (c && c.project && c.project === COMPOSE_PROJECT) {
|
|
63
|
+
if (c.name) ours.add(c.name);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
scan(c && c.name);
|
|
67
|
+
scan(c && c.image);
|
|
68
|
+
}
|
|
69
|
+
for (const p of processes) scan(p);
|
|
70
|
+
|
|
71
|
+
const hits = [...found];
|
|
72
|
+
return {
|
|
73
|
+
hasObservability: hits.length > 0,
|
|
74
|
+
found: hits.sort(),
|
|
75
|
+
ours: [...ours].sort(),
|
|
76
|
+
recommendation: hits.length > 0 ? 'skip' : 'provision',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when OUR compose stack is already running on the host. */
|
|
81
|
+
export function ownStackRunning(containers = []) {
|
|
82
|
+
return containers.some((c) => c && c.project === COMPOSE_PROJECT);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read running containers via `docker ps`. Returns [] (not throw) when Docker
|
|
87
|
+
* is absent/unreachable, so detection degrades to "process scan only".
|
|
88
|
+
*/
|
|
89
|
+
export async function listContainers() {
|
|
90
|
+
try {
|
|
91
|
+
// Tab-separated so names/images with spaces stay parseable.
|
|
92
|
+
const fmt = '{{.Names}}\t{{.Image}}\t{{.Label "com.docker.compose.project"}}';
|
|
93
|
+
const { stdout } = await execAsync(`docker ps --no-trunc --format '${fmt}'`, { timeout: 15000 });
|
|
94
|
+
return stdout
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((l) => l.trim())
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.map((line) => {
|
|
99
|
+
const [name = '', image = '', project = ''] = line.split('\t');
|
|
100
|
+
return { name, image, project };
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Best-effort process list (Linux/macOS). Returns [] on failure. */
|
|
108
|
+
export async function listProcesses() {
|
|
109
|
+
try {
|
|
110
|
+
const { stdout } = await execAsync('ps -eo args=', { timeout: 10000 });
|
|
111
|
+
return stdout.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
112
|
+
} catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether `docker` (and the compose plugin) are usable on this host. */
|
|
118
|
+
export async function dockerAvailable() {
|
|
119
|
+
try {
|
|
120
|
+
await execAsync('docker version --format "{{.Server.Version}}"', { timeout: 15000 });
|
|
121
|
+
} catch {
|
|
122
|
+
return { docker: false, compose: false };
|
|
123
|
+
}
|
|
124
|
+
let compose = false;
|
|
125
|
+
try {
|
|
126
|
+
await execAsync('docker compose version', { timeout: 15000 });
|
|
127
|
+
compose = true;
|
|
128
|
+
} catch {
|
|
129
|
+
compose = false;
|
|
130
|
+
}
|
|
131
|
+
return { docker: true, compose };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Full host observability probe: containers + processes + docker availability,
|
|
136
|
+
* classified. This is what the /observability/status route returns.
|
|
137
|
+
*/
|
|
138
|
+
export async function detectObservability() {
|
|
139
|
+
const [containers, processes, docker] = await Promise.all([
|
|
140
|
+
listContainers(),
|
|
141
|
+
listProcesses(),
|
|
142
|
+
dockerAvailable(),
|
|
143
|
+
]);
|
|
144
|
+
const classification = classifyObservability({ containers, processes });
|
|
145
|
+
return {
|
|
146
|
+
...classification,
|
|
147
|
+
ownStackRunning: ownStackRunning(containers),
|
|
148
|
+
docker,
|
|
149
|
+
containersScanned: containers.length,
|
|
150
|
+
};
|
|
151
|
+
}
|