Declarative Grafana / Prometheus / Loki / Tempo observability stack
Deploys the whole stack into the observability namespace of the local cluster with Kustomize alone — no helm install, no dashboard created by hand in the UI. The Makefile carries every command: deploy, dashboards, diagnostics, PVC flush, teardown. Dashboards are files, never UI objects. Each carries a stable uid, which is what keeps URLs, cross-links and alerts alive across redeployments, and references datasources by stable uid rather than by name. The dashboard ConfigMap is generated without a name-suffix hash so applying it updates it in place and Grafana reloads without a restart, while the provisioning ConfigMaps keep their hash precisely so that a change there does force one. Prometheus scrapes the kubelet /metrics/resource endpoint rather than /metrics/cadvisor: on this cluster cadvisor exposes machine_* series only and no container_* at all, which would leave every CPU and memory panel empty. Alloy ships every pod's logs to Loki through the Kubernetes API, so no /var/log mount is needed. Tempo's metrics_generator feeds span metrics and the service graph back into Prometheus, and the datasources are wired so a log line's trace_id opens the matching trace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.tmp
|
||||||
|
# Real secrets: only secret.example.yaml is versioned
|
||||||
|
k8s/secret.yaml
|
||||||
|
k8s/prometheus/secret.yaml
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# infra-observability -- Grafana / Prometheus / Loki / Tempo stack (+ Alloy)
|
||||||
|
# Fully declarative: kustomize + kubectl. No imperative `helm install`.
|
||||||
|
#
|
||||||
|
# make -> help
|
||||||
|
# make deploy -> deploy everything
|
||||||
|
# make diag -> full diagnostics
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
SHELL := /bin/bash
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
# Every target prints its own output, so commands are not echoed.
|
||||||
|
.SILENT:
|
||||||
|
|
||||||
|
NS ?= observability
|
||||||
|
CTX ?= orbstack
|
||||||
|
KUBECTL := kubectl --context=$(CTX)
|
||||||
|
K8S := k8s
|
||||||
|
DASHBOARDS := $(K8S)/grafana/dashboards
|
||||||
|
INGRESS_IP ?= 192.168.139.101
|
||||||
|
HOSTS := grafana.sttlab.local prometheus.sttlab.local alloy.sttlab.local
|
||||||
|
|
||||||
|
# Components backed by a PVC (this is also the flush order)
|
||||||
|
STATEFUL := prometheus loki tempo grafana
|
||||||
|
# Restartable workloads
|
||||||
|
WORKLOADS := deployment/prometheus deployment/loki deployment/tempo deployment/grafana daemonset/alloy
|
||||||
|
|
||||||
|
C_OK := \033[0;32m
|
||||||
|
C_WARN := \033[0;33m
|
||||||
|
C_ERR := \033[0;31m
|
||||||
|
C_HEAD := \033[1;36m
|
||||||
|
C_OFF := \033[0m
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Help
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show this help
|
||||||
|
printf "$(C_HEAD)infra-observability$(C_OFF) -- namespace $(C_OK)$(NS)$(C_OFF) / context $(C_OK)$(CTX)$(C_OFF)\n\n"
|
||||||
|
awk 'BEGIN {FS = ":.*?## "} \
|
||||||
|
/^[a-zA-Z0-9_-]+:.*?## / {printf " $(C_OK)%-24s$(C_OFF) %s\n", $$1, $$2} \
|
||||||
|
/^##@/ {printf "\n$(C_HEAD)%s$(C_OFF)\n", substr($$0, 5)}' $(MAKEFILE_LIST)
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
##@ Deployment
|
||||||
|
|
||||||
|
.PHONY: deploy
|
||||||
|
deploy: check-context secret ## Deploy the whole stack (runtimes + dashboards)
|
||||||
|
$(KUBECTL) apply -k $(K8S)
|
||||||
|
$(MAKE) --no-print-directory wait
|
||||||
|
|
||||||
|
.PHONY: deploy-runtime
|
||||||
|
deploy-runtime: check-context secret ## Deploy runtimes only (no dashboards)
|
||||||
|
$(KUBECTL) apply -f $(K8S)/namespace.yaml -f $(K8S)/secret.yaml
|
||||||
|
$(KUBECTL) apply -k $(K8S)/prometheus
|
||||||
|
$(KUBECTL) apply -k $(K8S)/loki
|
||||||
|
$(KUBECTL) apply -k $(K8S)/tempo
|
||||||
|
$(KUBECTL) apply -k $(K8S)/alloy
|
||||||
|
$(KUBECTL) apply -k $(K8S)/grafana
|
||||||
|
$(KUBECTL) apply -k $(K8S)/ingress
|
||||||
|
$(MAKE) --no-print-directory wait
|
||||||
|
|
||||||
|
.PHONY: prometheus loki tempo grafana alloy ingress
|
||||||
|
prometheus: check-context ## Deploy/update Prometheus only
|
||||||
|
$(KUBECTL) apply -k $(K8S)/prometheus
|
||||||
|
loki: check-context ## Deploy/update Loki only
|
||||||
|
$(KUBECTL) apply -k $(K8S)/loki
|
||||||
|
tempo: check-context ## Deploy/update Tempo only
|
||||||
|
$(KUBECTL) apply -k $(K8S)/tempo
|
||||||
|
grafana: check-context ## Deploy/update the Grafana runtime only (datasources included)
|
||||||
|
$(KUBECTL) apply -k $(K8S)/grafana
|
||||||
|
alloy: check-context ## Deploy/update Alloy only (log collection)
|
||||||
|
$(KUBECTL) apply -k $(K8S)/alloy
|
||||||
|
ingress: check-context ## Deploy/update the Ingresses and the certificate
|
||||||
|
$(KUBECTL) apply -k $(K8S)/ingress
|
||||||
|
|
||||||
|
.PHONY: secret
|
||||||
|
secret: ## Generate k8s/secret.yaml (random admin password) if absent
|
||||||
|
if [ ! -f $(K8S)/secret.yaml ]; then \
|
||||||
|
pw=$$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 20); \
|
||||||
|
sed -e '1s|.*|# Generated by `make secret` -- NOT versioned (see .gitignore).|' \
|
||||||
|
-e "s|<CHANGE_ME>|$$pw|" $(K8S)/secret.example.yaml > $(K8S)/secret.yaml; \
|
||||||
|
printf "$(C_OK)secret.yaml generated$(C_OFF) -- admin password: $$pw\n"; \
|
||||||
|
else \
|
||||||
|
printf "secret.yaml already present (use make grafana-password to read it)\n"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
##@ Dashboards (declarative, stable UIDs)
|
||||||
|
|
||||||
|
.PHONY: dashboards
|
||||||
|
dashboards: check-context dashboards-check ## Apply dashboards only (no Grafana restart)
|
||||||
|
$(KUBECTL) apply -k $(DASHBOARDS)
|
||||||
|
printf "$(C_OK)grafana-dashboards ConfigMap updated$(C_OFF) -- visible in Grafana within ~70s\n"
|
||||||
|
printf " (kubelet volume sync ~60s + provisioner poll 10s, no restart).\n"
|
||||||
|
printf " For immediate effect: $(C_WARN)make restart-grafana$(C_OFF)\n"
|
||||||
|
|
||||||
|
.PHONY: dashboards-sync
|
||||||
|
dashboards-sync: ## Regenerate the .json list in dashboards/kustomization.yaml
|
||||||
|
cd $(DASHBOARDS) && python3 -c "import glob,re,pathlib; \
|
||||||
|
p=pathlib.Path('kustomization.yaml'); s=p.read_text(); \
|
||||||
|
files=sorted(glob.glob('*.json')); \
|
||||||
|
block=''.join(' - %s\n' % f for f in files); \
|
||||||
|
s=re.sub(r'(# >>> dashboards.*?>>>\n).*?( # <<< dashboards <<<)', lambda m: m.group(1)+block+m.group(2), s, flags=re.S); \
|
||||||
|
p.write_text(s); print('%d dashboard(s) referenced' % len(files))"
|
||||||
|
$(MAKE) --no-print-directory dashboards-check
|
||||||
|
|
||||||
|
.PHONY: dashboards-check
|
||||||
|
dashboards-check: ## Validate the JSON files and UID uniqueness/stability
|
||||||
|
fail=0; \
|
||||||
|
for f in $(DASHBOARDS)/*.json; do \
|
||||||
|
jq -e . "$$f" >/dev/null 2>&1 || { printf "$(C_ERR)invalid JSON$(C_OFF): %s\n" "$$f"; fail=1; continue; }; \
|
||||||
|
uid=$$(jq -r '.uid // empty' "$$f"); \
|
||||||
|
[ -n "$$uid" ] || { printf "$(C_ERR)missing uid$(C_OFF): %s\n" "$$f"; fail=1; }; \
|
||||||
|
base=$$(basename "$$f" .json); \
|
||||||
|
[ "$$uid" = "$$base" ] || printf "$(C_WARN)filename != uid$(C_OFF): %s (uid=%s)\n" "$$f" "$$uid"; \
|
||||||
|
[ "$$(jq -r '.id // "null"' "$$f")" = "null" ] || printf "$(C_WARN)non-null id field$(C_OFF): %s (remove it)\n" "$$f"; \
|
||||||
|
grep -q -- "- $$base.json" $(DASHBOARDS)/kustomization.yaml || { printf "$(C_ERR)not referenced in kustomization.yaml$(C_OFF): %s (run make dashboards-sync)\n" "$$f"; fail=1; }; \
|
||||||
|
done; \
|
||||||
|
dup=$$(jq -r '.uid' $(DASHBOARDS)/*.json | sort | uniq -d); \
|
||||||
|
[ -z "$$dup" ] || { printf "$(C_ERR)duplicate UIDs$(C_OFF): %s\n" "$$dup"; fail=1; }; \
|
||||||
|
[ $$fail -eq 0 ] && printf "$(C_OK)dashboards OK$(C_OFF) (%s)\n" "$$(ls $(DASHBOARDS)/*.json | wc -l | tr -d ' ')"; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
.PHONY: dashboards-list
|
||||||
|
dashboards-list: ## List the dashboards actually loaded by Grafana
|
||||||
|
auth="$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_USER}' | base64 -d):$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_PASSWORD}' | base64 -d)"; \
|
||||||
|
$(KUBECTL) -n $(NS) exec deployment/grafana -- curl -s -u "$$auth" 'http://localhost:3000/api/search?type=dash-db' \
|
||||||
|
| jq -r '.[] | "\(.uid)\t\(.folderTitle // "General")\t\(.title)"'
|
||||||
|
|
||||||
|
##@ Diagnostics
|
||||||
|
|
||||||
|
.PHONY: status
|
||||||
|
status: ## Overview (pods, services, PVCs, ingresses)
|
||||||
|
$(KUBECTL) -n $(NS) get pods -o wide
|
||||||
|
echo
|
||||||
|
$(KUBECTL) -n $(NS) get svc,pvc,ingress
|
||||||
|
|
||||||
|
.PHONY: diag
|
||||||
|
diag: ## Full diagnostics: state, PVCs, endpoints, certificate, events
|
||||||
|
printf "$(C_HEAD)== Pods ==$(C_OFF)\n"; $(KUBECTL) -n $(NS) get pods -o wide
|
||||||
|
printf "\n$(C_HEAD)== PVCs ==$(C_OFF)\n"; $(KUBECTL) -n $(NS) get pvc
|
||||||
|
printf "\n$(C_HEAD)== Endpoints ==$(C_OFF)\n"; $(KUBECTL) -n $(NS) get endpoints
|
||||||
|
printf "\n$(C_HEAD)== Ingresses ==$(C_OFF)\n"; $(KUBECTL) -n $(NS) get ingress
|
||||||
|
printf "\n$(C_HEAD)== Certificate ==$(C_OFF)\n";$(KUBECTL) -n $(NS) get certificate 2>/dev/null || echo "(cert-manager absent)"
|
||||||
|
printf "\n$(C_HEAD)== ConfigMaps ==$(C_OFF)\n";$(KUBECTL) -n $(NS) get cm
|
||||||
|
printf "\n$(C_HEAD)== Containers not ready ==$(C_OFF)\n"
|
||||||
|
$(KUBECTL) -n $(NS) get pods -o json | jq -r '.items[] | select(.status.containerStatuses != null) | .status.containerStatuses[] | select(.ready == false) | "\(.name)\trestarts=\(.restartCount)\t\(.state | keys[0]): \(.state[keys[0]].reason // "-")"' || true
|
||||||
|
printf "\n$(C_HEAD)== LibreChat scrape token ==$(C_OFF)\n"
|
||||||
|
$(MAKE) --no-print-directory check-metrics-token || true
|
||||||
|
printf "\n$(C_HEAD)== Last 15 events ==$(C_OFF)\n"
|
||||||
|
$(KUBECTL) -n $(NS) get events --sort-by=.lastTimestamp | tail -15
|
||||||
|
|
||||||
|
.PHONY: smoke
|
||||||
|
smoke: ## End-to-end checks: component health, datasources, dashboards, targets
|
||||||
|
# The Loki and Tempo images ship neither curl nor wget, so probes run from the
|
||||||
|
# Grafana pod, which has curl and sees the same Services as the datasources.
|
||||||
|
probe() { $(KUBECTL) -n $(NS) exec deployment/grafana -- curl -s --max-time 10 "$$1"; }; \
|
||||||
|
auth="$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_USER}' | base64 -d):$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_PASSWORD}' | base64 -d)"; \
|
||||||
|
printf "$(C_HEAD)== Component health ==$(C_OFF)\n"; \
|
||||||
|
printf " %-12s %s\n" "prometheus" "$$(probe http://prometheus:9090/-/ready)"; \
|
||||||
|
printf " %-12s %s\n" "loki" "$$(probe http://loki:3100/ready)"; \
|
||||||
|
printf " %-12s %s\n" "tempo" "$$(probe http://tempo:3200/ready)"; \
|
||||||
|
printf " %-12s %s\n" "grafana" "$$(probe http://localhost:3000/api/health | jq -c .)"; \
|
||||||
|
printf " %-12s %s\n" "alloy" "$$(probe http://alloy:12345/-/ready)"; \
|
||||||
|
printf "\n$(C_HEAD)== Grafana datasources ==$(C_OFF)\n"; \
|
||||||
|
for uid in prometheus loki tempo; do \
|
||||||
|
r=$$($(KUBECTL) -n $(NS) exec deployment/grafana -- curl -s -u "$$auth" -X POST "http://localhost:3000/api/datasources/uid/$$uid/health" | jq -r '"\(.status) \(.message)"'); \
|
||||||
|
printf " %-12s %s\n" "$$uid" "$$r"; \
|
||||||
|
done; \
|
||||||
|
printf "\n$(C_HEAD)== Provisioned dashboards ==$(C_OFF)\n"; \
|
||||||
|
$(KUBECTL) -n $(NS) exec deployment/grafana -- curl -s -u "$$auth" 'http://localhost:3000/api/search?type=dash-db' \
|
||||||
|
| jq -r '.[] | " \(.uid) \(.folderTitle // "General")/\(.title)"'; \
|
||||||
|
printf "\n$(C_HEAD)== Namespaces seen by Loki ==$(C_OFF)\n"; \
|
||||||
|
probe 'http://loki:3100/loki/api/v1/label/namespace/values' | jq -r '" " + (.data // [] | join(", "))'; \
|
||||||
|
printf "\n$(C_HEAD)== Prometheus targets ==$(C_OFF)\n"; \
|
||||||
|
probe http://prometheus:9090/api/v1/targets | jq -r '.data.activeTargets | group_by(.health)[] | " \(.[0].health)\t\(length)"'
|
||||||
|
|
||||||
|
.PHONY: targets
|
||||||
|
targets: ## List Prometheus scrape targets and their state
|
||||||
|
$(KUBECTL) -n $(NS) exec deployment/prometheus -- wget -qO- http://localhost:9090/api/v1/targets \
|
||||||
|
| jq -r '.data.activeTargets[] | "\(.health)\t\(.labels.job)\t\(.scrapeUrl)\t\(.lastError // "")"' | sort
|
||||||
|
|
||||||
|
.PHONY: check-metrics-token
|
||||||
|
check-metrics-token: ## Check the LibreChat token matches across both namespaces
|
||||||
|
a=$$($(KUBECTL) -n $(NS) get secret librechat-metrics -o jsonpath='{.data.token}' 2>/dev/null | base64 -d); \
|
||||||
|
b=$$($(KUBECTL) -n librechat get secret librechat-secrets -o jsonpath='{.data.METRICS_SECRET}' 2>/dev/null | base64 -d); \
|
||||||
|
if [ -z "$$a" ] || [ -z "$$b" ]; then \
|
||||||
|
printf "$(C_WARN)token not found$(C_OFF) (observability/librechat-metrics=%s, librechat/librechat-secrets=%s)\n" "$${a:+present}$${a:-absent}" "$${b:+present}$${b:-absent}"; \
|
||||||
|
elif [ "$$a" = "$$b" ]; then \
|
||||||
|
printf "$(C_OK)LibreChat token matches$(C_OFF) across observability and librechat\n"; \
|
||||||
|
else \
|
||||||
|
printf "$(C_ERR)DRIFT$(C_OFF): the two Secrets hold different values.\n"; \
|
||||||
|
printf " The librechat scrape will fall back to 401. Realign both copies.\n"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: logs-prometheus logs-loki logs-tempo logs-grafana logs-alloy
|
||||||
|
logs-prometheus: ## Follow Prometheus logs
|
||||||
|
$(KUBECTL) -n $(NS) logs -f deployment/prometheus --tail=100
|
||||||
|
logs-loki: ## Follow Loki logs
|
||||||
|
$(KUBECTL) -n $(NS) logs -f deployment/loki --tail=100
|
||||||
|
logs-tempo: ## Follow Tempo logs
|
||||||
|
$(KUBECTL) -n $(NS) logs -f deployment/tempo --tail=100
|
||||||
|
logs-grafana: ## Follow Grafana logs
|
||||||
|
$(KUBECTL) -n $(NS) logs -f deployment/grafana --tail=100
|
||||||
|
logs-alloy: ## Follow Alloy logs
|
||||||
|
$(KUBECTL) -n $(NS) logs -f daemonset/alloy --tail=100
|
||||||
|
|
||||||
|
.PHONY: events
|
||||||
|
events: ## Namespace events, oldest to newest
|
||||||
|
$(KUBECTL) -n $(NS) get events --sort-by=.lastTimestamp
|
||||||
|
|
||||||
|
.PHONY: top
|
||||||
|
top: ## Pod CPU/memory usage
|
||||||
|
$(KUBECTL) -n $(NS) top pods 2>/dev/null || echo "metrics-server unavailable"
|
||||||
|
|
||||||
|
.PHONY: urls
|
||||||
|
urls: ## Show access URLs
|
||||||
|
printf " Grafana https://grafana.sttlab.local\n"
|
||||||
|
printf " Prometheus https://prometheus.sttlab.local\n"
|
||||||
|
printf " Alloy https://alloy.sttlab.local\n"
|
||||||
|
printf "\n In-cluster: http://prometheus:9090 - http://loki:3100 - http://tempo:3200\n"
|
||||||
|
printf " OTLP : tempo.$(NS).svc.cluster.local:4317 (gRPC) / :4318 (HTTP)\n"
|
||||||
|
|
||||||
|
.PHONY: hosts
|
||||||
|
hosts: ## Show the required /etc/hosts entries
|
||||||
|
printf "# Add to /etc/hosts:\n"
|
||||||
|
for h in $(HOSTS); do printf "$(INGRESS_IP)\t%s\n" "$$h"; done
|
||||||
|
|
||||||
|
.PHONY: grafana-password
|
||||||
|
grafana-password: ## Show the Grafana admin credentials
|
||||||
|
printf "user : %s\n" "$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_USER}' | base64 -d)"
|
||||||
|
printf "password : %s\n" "$$($(KUBECTL) -n $(NS) get secret observability-secrets -o jsonpath='{.data.GF_SECURITY_ADMIN_PASSWORD}' | base64 -d)"
|
||||||
|
|
||||||
|
.PHONY: pf-grafana pf-prometheus pf-loki pf-tempo pf-alloy
|
||||||
|
pf-grafana: ## Port-forward Grafana on :3000
|
||||||
|
$(KUBECTL) -n $(NS) port-forward svc/grafana 3000:3000
|
||||||
|
pf-prometheus: ## Port-forward Prometheus on :9090
|
||||||
|
$(KUBECTL) -n $(NS) port-forward svc/prometheus 9090:9090
|
||||||
|
pf-loki: ## Port-forward Loki on :3100
|
||||||
|
$(KUBECTL) -n $(NS) port-forward svc/loki 3100:3100
|
||||||
|
pf-tempo: ## Port-forward Tempo on :3200
|
||||||
|
$(KUBECTL) -n $(NS) port-forward svc/tempo 3200:3200
|
||||||
|
pf-alloy: ## Port-forward the Alloy UI on :12345
|
||||||
|
$(KUBECTL) -n $(NS) port-forward svc/alloy 12345:12345
|
||||||
|
|
||||||
|
##@ Operations
|
||||||
|
|
||||||
|
.PHONY: wait
|
||||||
|
wait: ## Wait for every workload to become ready
|
||||||
|
for w in $(WORKLOADS); do \
|
||||||
|
printf " %-24s " "$$w"; \
|
||||||
|
$(KUBECTL) -n $(NS) rollout status $$w --timeout=300s 2>&1 | tail -1; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: restart
|
||||||
|
restart: ## Restart every workload
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart $(WORKLOADS)
|
||||||
|
$(MAKE) --no-print-directory wait
|
||||||
|
|
||||||
|
.PHONY: restart-prometheus restart-loki restart-tempo restart-grafana restart-alloy
|
||||||
|
restart-prometheus: ## Restart Prometheus
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart deployment/prometheus && $(KUBECTL) -n $(NS) rollout status deployment/prometheus
|
||||||
|
restart-loki: ## Restart Loki
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart deployment/loki && $(KUBECTL) -n $(NS) rollout status deployment/loki
|
||||||
|
restart-tempo: ## Restart Tempo
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart deployment/tempo && $(KUBECTL) -n $(NS) rollout status deployment/tempo
|
||||||
|
restart-grafana: ## Restart Grafana (dashboards picked up immediately)
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart deployment/grafana && $(KUBECTL) -n $(NS) rollout status deployment/grafana
|
||||||
|
restart-alloy: ## Restart Alloy
|
||||||
|
$(KUBECTL) -n $(NS) rollout restart daemonset/alloy && $(KUBECTL) -n $(NS) rollout status daemonset/alloy
|
||||||
|
|
||||||
|
.PHONY: reload-prometheus
|
||||||
|
reload-prometheus: ## Hot-reload the Prometheus config (no restart)
|
||||||
|
$(KUBECTL) -n $(NS) exec deployment/prometheus -- wget -q --post-data='' -O- http://localhost:9090/-/reload \
|
||||||
|
&& printf "$(C_OK)Prometheus configuration reloaded$(C_OFF)\n"
|
||||||
|
|
||||||
|
##@ Persistence
|
||||||
|
|
||||||
|
.PHONY: pvc
|
||||||
|
pvc: ## PVC state and the volumes behind them
|
||||||
|
$(KUBECTL) -n $(NS) get pvc -o custom-columns='NAME:.metadata.name,STATUS:.status.phase,CAPACITY:.status.capacity.storage,SC:.spec.storageClassName,VOLUME:.spec.volumeName'
|
||||||
|
echo
|
||||||
|
$(KUBECTL) -n $(NS) get pods -o json | jq -r '.items[] | . as $$p | (.spec.volumes[]? | select(.persistentVolumeClaim) | "\($$p.metadata.name)\t-> \(.persistentVolumeClaim.claimName)")'
|
||||||
|
|
||||||
|
.PHONY: flush-pvc
|
||||||
|
flush-pvc: ## DESTRUCTIVE: wipe all persisted data (CONFIRM=yes)
|
||||||
|
$(call confirm,Wipe ALL data (metrics, logs, traces, Grafana))
|
||||||
|
for c in $(STATEFUL); do $(MAKE) --no-print-directory _flush COMP=$$c; done
|
||||||
|
$(MAKE) --no-print-directory wait
|
||||||
|
|
||||||
|
.PHONY: flush-prometheus flush-loki flush-tempo flush-grafana
|
||||||
|
flush-prometheus: ## DESTRUCTIVE: wipe the Prometheus TSDB (CONFIRM=yes)
|
||||||
|
$(call confirm,Wipe the Prometheus TSDB)
|
||||||
|
$(MAKE) --no-print-directory _flush COMP=prometheus
|
||||||
|
flush-loki: ## DESTRUCTIVE: wipe the Loki logs (CONFIRM=yes)
|
||||||
|
$(call confirm,Wipe the Loki logs)
|
||||||
|
$(MAKE) --no-print-directory _flush COMP=loki
|
||||||
|
flush-tempo: ## DESTRUCTIVE: wipe the Tempo traces (CONFIRM=yes)
|
||||||
|
$(call confirm,Wipe the Tempo traces)
|
||||||
|
$(MAKE) --no-print-directory _flush COMP=tempo
|
||||||
|
flush-grafana: ## DESTRUCTIVE: wipe the Grafana database (CONFIRM=yes)
|
||||||
|
$(call confirm,Wipe the Grafana database (users and prefs; dashboards are reprovisioned))
|
||||||
|
$(MAKE) --no-print-directory _flush COMP=grafana
|
||||||
|
|
||||||
|
# Internal target: scale down -> delete PVC -> recreate -> scale up.
|
||||||
|
.PHONY: _flush
|
||||||
|
_flush:
|
||||||
|
printf "$(C_WARN)flushing %s$(C_OFF)\n" "$(COMP)"
|
||||||
|
$(KUBECTL) -n $(NS) scale deployment/$(COMP) --replicas=0
|
||||||
|
$(KUBECTL) -n $(NS) wait --for=delete pod -l app=$(COMP) --timeout=120s 2>/dev/null || true
|
||||||
|
$(KUBECTL) -n $(NS) delete pvc $(COMP)-data --ignore-not-found --wait=true
|
||||||
|
$(KUBECTL) apply -k $(K8S)/$(COMP)
|
||||||
|
$(KUBECTL) -n $(NS) scale deployment/$(COMP) --replicas=1
|
||||||
|
|
||||||
|
##@ Validation and teardown
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Render the generated manifests to stdout
|
||||||
|
kustomize build $(K8S)
|
||||||
|
|
||||||
|
.PHONY: diff
|
||||||
|
diff: check-context ## Diff the manifests against the cluster state
|
||||||
|
$(KUBECTL) diff -k $(K8S) || true
|
||||||
|
|
||||||
|
.PHONY: validate
|
||||||
|
validate: dashboards-check ## Validate the manifests (build + server dry-run)
|
||||||
|
kustomize build $(K8S) >/dev/null && printf "$(C_OK)kustomize build OK$(C_OFF)\n"
|
||||||
|
$(KUBECTL) apply -k $(K8S) --dry-run=server >/dev/null && printf "$(C_OK)server dry-run OK$(C_OFF)\n"
|
||||||
|
|
||||||
|
.PHONY: undeploy
|
||||||
|
undeploy: ## DESTRUCTIVE: delete the stack, PVCs kept (CONFIRM=yes)
|
||||||
|
$(call confirm,Delete the stack (PVCs are KEPT))
|
||||||
|
$(KUBECTL) delete -k $(K8S) --ignore-not-found
|
||||||
|
|
||||||
|
.PHONY: destroy
|
||||||
|
destroy: ## DESTRUCTIVE: delete the stack AND the PVCs (CONFIRM=yes)
|
||||||
|
$(call confirm,Delete the stack AND all data)
|
||||||
|
$(KUBECTL) delete -k $(K8S) --ignore-not-found
|
||||||
|
$(KUBECTL) -n $(NS) delete pvc --all --ignore-not-found
|
||||||
|
|
||||||
|
.PHONY: check-context
|
||||||
|
check-context:
|
||||||
|
cur=$$(kubectl config current-context); \
|
||||||
|
if [ "$$cur" != "$(CTX)" ]; then \
|
||||||
|
printf "$(C_ERR)current context '%s' != '$(CTX)'$(C_OFF) (commands explicitly target $(CTX))\n" "$$cur"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Guard rail for destructive targets: requires CONFIRM=yes
|
||||||
|
define confirm
|
||||||
|
@if [ "$(CONFIRM)" != "yes" ]; then \
|
||||||
|
printf "$(C_ERR)Destructive operation$(C_OFF): $(1)\n"; \
|
||||||
|
printf "Re-run with $(C_WARN)CONFIRM=yes$(C_OFF): make $(C_WARN)$@ CONFIRM=yes$(C_OFF)\n"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# Infra Observability (Kubernetes / declarative)
|
||||||
|
|
||||||
|
**Grafana 13.2 / Prometheus 3.14 / Loki 3.7 / Tempo 3.0** plus **Alloy 1.19**,
|
||||||
|
deployed fully declaratively (Kustomize) into the `observability` namespace of
|
||||||
|
the local `orbstack` cluster.
|
||||||
|
|
||||||
|
Nothing is imperative: no `helm install`, no dashboard hand-created in the UI.
|
||||||
|
The entire desired state lives in this repository and `make deploy` reconciles it.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
Makefile # Every command (make = help)
|
||||||
|
k8s/
|
||||||
|
├── kustomization.yaml # Root Kustomize manifest
|
||||||
|
├── namespace.yaml # observability namespace
|
||||||
|
├── secret.yaml # Grafana admin credentials (GITIGNORED)
|
||||||
|
├── secret.example.yaml # Secret template
|
||||||
|
├── prometheus/
|
||||||
|
│ ├── prometheus.yaml # Scrape config (Kubernetes SD)
|
||||||
|
│ ├── rbac.yaml # SA + discovery ClusterRole
|
||||||
|
│ ├── secret.yaml # LibreChat scrape token (GITIGNORED)
|
||||||
|
│ ├── secret.example.yaml # Token template
|
||||||
|
│ ├── pvc.yaml # Persistent TSDB (10Gi)
|
||||||
|
│ ├── deployment.yaml # Prometheus v3.14.0
|
||||||
|
│ └── service.yaml # :9090
|
||||||
|
├── loki/
|
||||||
|
│ ├── loki.yaml # Monolithic config, filesystem storage
|
||||||
|
│ ├── pvc.yaml # Chunks + index (10Gi)
|
||||||
|
│ ├── deployment.yaml # Loki 3.7.6 (-target=all)
|
||||||
|
│ └── service.yaml # :3100 (HTTP) / :9096 (gRPC)
|
||||||
|
├── tempo/
|
||||||
|
│ ├── tempo.yaml # Monolithic config + metrics-generator
|
||||||
|
│ ├── pvc.yaml # WAL + trace blocks (10Gi)
|
||||||
|
│ ├── deployment.yaml # Tempo 3.0.3 (-target=all)
|
||||||
|
│ └── service.yaml # :3200, OTLP :4317/:4318, Zipkin :9411
|
||||||
|
├── alloy/
|
||||||
|
│ ├── config.alloy # Pod logs -> Loki pipeline
|
||||||
|
│ ├── rbac.yaml # SA + ClusterRole (including pods/log)
|
||||||
|
│ ├── daemonset.yaml # Alloy v1.19.0 (one pod per node)
|
||||||
|
│ └── service.yaml # :12345 (UI + /metrics)
|
||||||
|
├── grafana/
|
||||||
|
│ ├── provisioning/
|
||||||
|
│ │ ├── datasources.yaml # Datasources with stable UIDs
|
||||||
|
│ │ └── dashboard-providers.yaml # File provider
|
||||||
|
│ ├── dashboards/
|
||||||
|
│ │ ├── kustomization.yaml # ConfigMap WITHOUT hash (hot reload)
|
||||||
|
│ │ ├── obs-stack-health.json # uid: obs-stack-health
|
||||||
|
│ │ ├── obs-keycloak.json # uid: obs-keycloak
|
||||||
|
│ │ ├── obs-librechat.json # uid: obs-librechat
|
||||||
|
│ │ └── obs-classic-models-api.json
|
||||||
|
│ ├── pvc.yaml # Grafana SQLite database (5Gi)
|
||||||
|
│ ├── deployment.yaml # Grafana 13.2.0
|
||||||
|
│ └── service.yaml # :3000
|
||||||
|
└── ingress/
|
||||||
|
├── certificate.yaml # cert-manager Certificate (sttlab-local-ca)
|
||||||
|
└── ingress.yaml # Three NGINX Ingresses
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make # Help: every available target
|
||||||
|
make deploy # Deploy the whole stack
|
||||||
|
make hosts # /etc/hosts entries to add
|
||||||
|
make grafana-password # Admin credentials
|
||||||
|
make smoke # End-to-end verification
|
||||||
|
```
|
||||||
|
|
||||||
|
| URL | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `https://grafana.sttlab.local` | Grafana UI |
|
||||||
|
| `https://prometheus.sttlab.local` | Prometheus UI (targets, expression browser) |
|
||||||
|
| `https://alloy.sttlab.local` | Alloy UI (log pipeline state) |
|
||||||
|
|
||||||
|
Sending traces from an in-cluster application:
|
||||||
|
`tempo.observability.svc.cluster.local:4317` (OTLP gRPC) or `:4318` (OTLP HTTP).
|
||||||
|
|
||||||
|
## Declarative dashboards with stable UIDs
|
||||||
|
|
||||||
|
This is the centrepiece of the repository: **a dashboard only ever exists as a
|
||||||
|
versioned JSON file**, never as an object created through the UI.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
1. **One `.json` file per dashboard**, under `k8s/grafana/dashboards/`.
|
||||||
|
2. **The `uid` field is stable and never changes.** It is what keeps URLs
|
||||||
|
(`/d/<uid>/…`), cross-dashboard links, annotations and alerts alive across
|
||||||
|
every redeployment. By convention the filename equals the UID
|
||||||
|
(`obs-keycloak.json` -> `uid: obs-keycloak`).
|
||||||
|
3. **The `id` field must stay `null`** (or absent): it is an identifier internal
|
||||||
|
to a Grafana instance and is not portable.
|
||||||
|
4. **Datasources are referenced by stable UID**, never by name:
|
||||||
|
`"datasource": { "type": "prometheus", "uid": "prometheus" }`.
|
||||||
|
Available UIDs: `prometheus`, `loki`, `tempo`.
|
||||||
|
5. `allowUiUpdates: false`: any change made in the UI is overwritten on the next
|
||||||
|
reload. The file is the source of truth.
|
||||||
|
|
||||||
|
### Adding a dashboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp my-dashboard.json k8s/grafana/dashboards/obs-my-topic.json
|
||||||
|
# edit the JSON: "uid": "obs-my-topic", "id": null
|
||||||
|
make dashboards-sync # update the list in dashboards/kustomization.yaml
|
||||||
|
make dashboards # apply (without restarting Grafana)
|
||||||
|
```
|
||||||
|
|
||||||
|
`make dashboards-check` (run automatically by `dashboards` and `validate`)
|
||||||
|
verifies: valid JSON, `uid` present, `uid` unique, `id` null, file referenced in
|
||||||
|
the kustomization, filename/UID consistency.
|
||||||
|
|
||||||
|
### How the hot reload works
|
||||||
|
|
||||||
|
The `grafana-dashboards` ConfigMap is generated **without a name-suffix hash**
|
||||||
|
(`disableNameSuffixHash: true`), so `make dashboards` updates it *in place*: the
|
||||||
|
Grafana pod is not recreated and the file provider re-reads the directory
|
||||||
|
(`updateIntervalSeconds: 10`).
|
||||||
|
|
||||||
|
Measured end-to-end delay: **about 70 seconds** (kubelet ConfigMap volume sync
|
||||||
|
~60s + provider poll 10s). For immediate effect: `make restart-grafana`.
|
||||||
|
|
||||||
|
Conversely, the *provisioning* ConfigMaps (datasources, providers) **keep** their
|
||||||
|
hash suffix: those files are only read at startup, so the hash deliberately
|
||||||
|
forces a pod restart when they change.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
| PVC | Component | Size | Contents | Retention |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `prometheus-data` | Prometheus | 10Gi | TSDB | 15 d / 8 GB |
|
||||||
|
| `loki-data` | Loki | 10Gi | chunks + TSDB index | 168 h (compactor) |
|
||||||
|
| `tempo-data` | Tempo | 10Gi | WAL + trace blocks | 168 h |
|
||||||
|
| `grafana-data` | Grafana | 5Gi | SQLite database (users, prefs) | — |
|
||||||
|
|
||||||
|
StorageClass `local-path` (the cluster default), `ReadWriteOnce`. The Deployments
|
||||||
|
use `strategy: Recreate`: with an RWO volume the old pod must release it before
|
||||||
|
the new one can start.
|
||||||
|
|
||||||
|
Alloy's read positions live on a `hostPath` (`/var/lib/alloy`), so restarting the
|
||||||
|
DaemonSet does not re-ingest every log.
|
||||||
|
|
||||||
|
### Wiping data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make pvc # PVC state
|
||||||
|
make flush-loki CONFIRM=yes # wipe the logs only
|
||||||
|
make flush-prometheus CONFIRM=yes # wipe the TSDB only
|
||||||
|
make flush-pvc CONFIRM=yes # wipe EVERYTHING
|
||||||
|
```
|
||||||
|
|
||||||
|
The cycle is: `scale 0` -> wait for pod deletion -> `delete pvc` -> reapply the
|
||||||
|
kustomization (which recreates the PVC) -> `scale 1`. Every destructive target
|
||||||
|
requires `CONFIRM=yes`.
|
||||||
|
|
||||||
|
Dashboards are **not** affected by `flush-grafana`: they are reprovisioned from
|
||||||
|
the ConfigMaps at startup.
|
||||||
|
|
||||||
|
## Wiring the three signals
|
||||||
|
|
||||||
|
- **Prometheus** discovers its targets through the Kubernetes API: apiserver,
|
||||||
|
kubelet, container resource metrics, plus any Service or Pod carrying the
|
||||||
|
`prometheus.io/scrape: "true"` annotation (`prometheus.io/port`,
|
||||||
|
`prometheus.io/path`).
|
||||||
|
- **Alloy** reads every pod's logs through the Kubernetes API (no `/var/log`
|
||||||
|
mount) and pushes them to Loki with `namespace`, `pod`, `container`, `node`,
|
||||||
|
`app`, `component` and `job` labels.
|
||||||
|
- **Tempo** receives OTLP, and its `metrics_generator` pushes span metrics and
|
||||||
|
service graphs to Prometheus through its remote-write receiver
|
||||||
|
(`--web.enable-remote-write-receiver`).
|
||||||
|
- **Grafana correlations**: Loki -> Tempo (`TraceID` derived field),
|
||||||
|
Tempo -> Loki (`tracesToLogsV2`), Tempo -> Prometheus (`serviceMap`,
|
||||||
|
`tracesToMetrics`), Prometheus exemplars -> Tempo.
|
||||||
|
|
||||||
|
### Container resource metrics
|
||||||
|
|
||||||
|
Prometheus scrapes the kubelet's **`/metrics/resource`** endpoint, not
|
||||||
|
`/metrics/cadvisor`. On this cluster the cadvisor endpoint answers with `machine_*`
|
||||||
|
series only and exposes no `container_*` at all, so CPU and memory panels would
|
||||||
|
stay empty. `/metrics/resource` — the source metrics-server itself reads —
|
||||||
|
provides `container_cpu_usage_seconds_total` and
|
||||||
|
`container_memory_working_set_bytes` with `namespace`/`pod`/`container` labels.
|
||||||
|
|
||||||
|
kube-state-metrics is not deployed, so container restarts are derived from
|
||||||
|
changes in `container_start_time_seconds`.
|
||||||
|
|
||||||
|
## Connected applications
|
||||||
|
|
||||||
|
| Application | Traces | Metrics | Logs | Dashboard |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `keycloak` | OTel Java agent → `tempo:4318` | native `/metrics` (management port 9000) | JSON + MDC `trace_id` | `obs-keycloak` |
|
||||||
|
| `librechat` | OTel SDK → `tempo:4318` | built-in `/metrics`, Bearer token | stdout | `obs-librechat` |
|
||||||
|
| `classic-models-api` | `opentelemetry-instrument` → `tempo:4318` | django-prometheus `/metrics` | JSON + `otelTraceID` | `obs-classic-models-api` |
|
||||||
|
|
||||||
|
Their manifests live in the `infra-keycloak`, `infra-librechat` and
|
||||||
|
`classic-models-api` repositories; this repository only holds the platform.
|
||||||
|
|
||||||
|
Each application dashboard carries the same four sections: an overview of RED
|
||||||
|
indicators, HTTP traffic, a runtime section specific to the stack in use, and a
|
||||||
|
**Logs** row, followed by container resources.
|
||||||
|
|
||||||
|
The Logs row leans on Loki's `detected_level`, which is inferred at ingestion
|
||||||
|
time, so a single query works across all three log formats — Keycloak emits JSON,
|
||||||
|
LibreChat colour-coded text, and the Django API plain gunicorn access lines. No
|
||||||
|
per-application parser is needed. The volume panel is scoped to the application
|
||||||
|
container, while the two error panels cover the whole namespace so the
|
||||||
|
datastore's own failures surface alongside the application's. Where a log line
|
||||||
|
carries a `trace_id`, the Loki datasource's derived field turns it into a link
|
||||||
|
straight to the matching trace in Tempo.
|
||||||
|
|
||||||
|
### Connecting an application
|
||||||
|
|
||||||
|
1. **Traces** — point OTLP at Tempo:
|
||||||
|
`OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo.observability.svc.cluster.local:4318`
|
||||||
|
(`:4317` for gRPC). RED span metrics and the service graph are then derived
|
||||||
|
automatically by Tempo's `metrics_generator`, with no extra instrumentation.
|
||||||
|
2. **Metrics** — annotate the **pod** (not the Service):
|
||||||
|
```yaml
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "9000"
|
||||||
|
prometheus.io/path: "/metrics"
|
||||||
|
```
|
||||||
|
3. **Logs** — nothing to do: Alloy collects every pod in the cluster. For Grafana
|
||||||
|
to jump from a log line to its trace, the line only needs to contain a
|
||||||
|
`trace_id` (the Loki datasource's derived field).
|
||||||
|
|
||||||
|
### Token-protected endpoints
|
||||||
|
|
||||||
|
Annotation-based discovery cannot carry credentials, so an authenticated target
|
||||||
|
gets its own `scrape_config` in `k8s/prometheus/prometheus.yaml`, with the token
|
||||||
|
mounted from a Secret — see the `librechat` job, which reads
|
||||||
|
`/etc/prometheus/secrets/librechat/token`.
|
||||||
|
|
||||||
|
That token must be **identical** on both sides: `METRICS_SECRET` in the
|
||||||
|
`librechat-secrets` secret (namespace `librechat`) and the `token` key of the
|
||||||
|
`librechat-metrics` secret (namespace `observability`, file
|
||||||
|
`k8s/prometheus/secret.yaml`, gitignored).
|
||||||
|
|
||||||
|
A Kubernetes Secret is namespaced and a pod can only mount ones from its own
|
||||||
|
namespace, so the same value has to exist twice — once for LibreChat, which
|
||||||
|
**verifies** the token, once for Prometheus, which **presents** it. The risk is
|
||||||
|
silent drift: regenerating one side without the other sends the scrape back to
|
||||||
|
401 without breaking anything else. `make check-metrics-token` (included in
|
||||||
|
`make diag`) compares both copies.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make status # pods, services, PVCs, ingresses
|
||||||
|
make diag # + endpoints, certificate, failing containers, events
|
||||||
|
make smoke # health of all five components, datasources, dashboards, targets
|
||||||
|
make targets # Prometheus targets and their last scrape error
|
||||||
|
make logs-loki # follow one component's logs
|
||||||
|
make top # CPU/memory usage
|
||||||
|
make pf-grafana # port-forward when the ingress is unavailable
|
||||||
|
make diff # difference between the repository and the cluster
|
||||||
|
make validate # kustomize build + server dry-run + dashboard checks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
`k8s/secret.yaml` and `k8s/prometheus/secret.yaml` are **gitignored**.
|
||||||
|
`make secret` generates the first from `secret.example.yaml` with a random admin
|
||||||
|
password if it does not exist (and is called automatically by `make deploy`).
|
||||||
|
|
||||||
|
The TLS certificate is not versioned either: it is issued by the existing
|
||||||
|
`sttlab-local-ca` cert-manager ClusterIssuer (`k8s/ingress/certificate.yaml`).
|
||||||
|
|
||||||
|
## Coexisting with what was already there
|
||||||
|
|
||||||
|
The `observability` namespace already hosts two deployments **not managed by this
|
||||||
|
repository**, left untouched:
|
||||||
|
|
||||||
|
- `jaeger` (installed by Helm) — it also exposes OTLP on `4317`/`4318`, but on its
|
||||||
|
own Service, so there is no conflict with Tempo. Since all three applications
|
||||||
|
now point at Tempo, Jaeger no longer receives anything; uninstall the Helm
|
||||||
|
release whenever you want a single trace backend.
|
||||||
|
- `audit-log-collector` (Fluent Bit) — writes to its own PVC.
|
||||||
|
|
||||||
|
`make undeploy` and `make destroy` only run `kubectl delete -k k8s/`, so they
|
||||||
|
touch nothing but the resources described here.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Grafana Alloy -- ships the logs of every pod in the cluster to Loki.
|
||||||
|
// Reads through the Kubernetes API (no /var/log mount required).
|
||||||
|
|
||||||
|
logging {
|
||||||
|
level = "info"
|
||||||
|
format = "logfmt"
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery.kubernetes "pods" {
|
||||||
|
role = "pod"
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery.relabel "pod_logs" {
|
||||||
|
targets = discovery.kubernetes.pods.targets
|
||||||
|
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_namespace"]
|
||||||
|
target_label = "namespace"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_name"]
|
||||||
|
target_label = "pod"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_container_name"]
|
||||||
|
target_label = "container"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_node_name"]
|
||||||
|
target_label = "node"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
|
||||||
|
target_label = "app"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_component"]
|
||||||
|
target_label = "component"
|
||||||
|
}
|
||||||
|
// job = <namespace>/<container>: the usual Loki convention.
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_container_name"]
|
||||||
|
separator = "/"
|
||||||
|
target_label = "job"
|
||||||
|
}
|
||||||
|
// Only collect containers on the current node (DaemonSet).
|
||||||
|
rule {
|
||||||
|
source_labels = ["__meta_kubernetes_pod_node_name"]
|
||||||
|
regex = env("HOSTNAME_NODE")
|
||||||
|
action = "keep"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loki.source.kubernetes "pods" {
|
||||||
|
targets = discovery.relabel.pod_logs.output
|
||||||
|
forward_to = [loki.write.default.receiver]
|
||||||
|
}
|
||||||
|
|
||||||
|
loki.write "default" {
|
||||||
|
endpoint {
|
||||||
|
url = "http://loki:3100/loki/api/v1/push"
|
||||||
|
}
|
||||||
|
external_labels = {
|
||||||
|
cluster = "orbstack",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: alloy
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/component: collector
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: alloy
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: alloy
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/component: collector
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "12345"
|
||||||
|
spec:
|
||||||
|
serviceAccountName: alloy
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: alloy
|
||||||
|
image: grafana/alloy:v1.19.0
|
||||||
|
args:
|
||||||
|
- run
|
||||||
|
- /etc/alloy/config.alloy
|
||||||
|
- --storage.path=/var/lib/alloy/data
|
||||||
|
- --server.http.listen-addr=0.0.0.0:12345
|
||||||
|
- --disable-reporting
|
||||||
|
env:
|
||||||
|
# Used by config.alloy to tail only the pods on the current node.
|
||||||
|
- name: HOSTNAME_NODE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 12345
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /-/ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/alloy
|
||||||
|
- name: storage
|
||||||
|
mountPath: /var/lib/alloy
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: alloy-config
|
||||||
|
- name: storage
|
||||||
|
# Read positions persisted on the node: no full log re-ingestion
|
||||||
|
# when the pod restarts.
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/alloy
|
||||||
|
type: DirectoryOrCreate
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- rbac.yaml
|
||||||
|
- daemonset.yaml
|
||||||
|
- service.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: alloy-config
|
||||||
|
files:
|
||||||
|
- config.alloy
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: alloy
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: observability-alloy
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
- nodes/proxy
|
||||||
|
- nodes/metrics
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- pods
|
||||||
|
- namespaces
|
||||||
|
- events
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods/log"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["discovery.k8s.io"]
|
||||||
|
resources: ["endpointslices"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- nonResourceURLs: ["/metrics"]
|
||||||
|
verbs: ["get"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: observability-alloy
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: observability-alloy
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: alloy
|
||||||
|
namespace: observability
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: alloy
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/component: collector
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: alloy
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 12345
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Grafana dashboards -- declarative provisioning.
|
||||||
|
#
|
||||||
|
# Rules:
|
||||||
|
# * one .json file per dashboard, named after its UID;
|
||||||
|
# * every JSON carries a STABLE "uid" field (never regenerated): that is what
|
||||||
|
# keeps URLs, cross-links and alerts alive across redeployments;
|
||||||
|
# * `disableNameSuffixHash: true` -> the ConfigMap always keeps the same name,
|
||||||
|
# so `kubectl apply` updates it in place and Grafana reloads WITHOUT
|
||||||
|
# restarting the pod, in about 70s (see dashboard-providers.yaml).
|
||||||
|
#
|
||||||
|
# Adding a dashboard = drop the .json here and add it to `files:` below.
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: grafana-dashboards
|
||||||
|
files:
|
||||||
|
# >>> dashboards (generated by `make dashboards-sync`) >>>
|
||||||
|
- obs-classic-models-api.json
|
||||||
|
- obs-keycloak.json
|
||||||
|
- obs-librechat.json
|
||||||
|
- obs-stack-health.json
|
||||||
|
# <<< dashboards <<<
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
disableNameSuffixHash: true
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/component: dashboards
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,437 @@
|
|||||||
|
{
|
||||||
|
"uid": "obs-stack-health",
|
||||||
|
"title": "Observability — Stack Health",
|
||||||
|
"description": "Health of the Prometheus / Loki / Tempo / Alloy stack. Reference dashboard: stable UID, declaratively provisioned.",
|
||||||
|
"tags": [
|
||||||
|
"observability",
|
||||||
|
"stack",
|
||||||
|
"provisioned"
|
||||||
|
],
|
||||||
|
"editable": false,
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timezone": "browser",
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "namespace",
|
||||||
|
"label": "Namespace",
|
||||||
|
"type": "query",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"query": "label_values(kube_pod_info, namespace)",
|
||||||
|
"refresh": 2,
|
||||||
|
"includeAll": true,
|
||||||
|
"multi": true,
|
||||||
|
"current": {
|
||||||
|
"text": "observability",
|
||||||
|
"value": "observability"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "row",
|
||||||
|
"title": "Collection",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"collapsed": false,
|
||||||
|
"panels": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Prometheus targets UP",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 6,
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum(up)",
|
||||||
|
"legendFormat": "up"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Prometheus targets DOWN",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 6,
|
||||||
|
"x": 6,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum(up == bool 0)",
|
||||||
|
"legendFormat": "down"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"textMode": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Log lines ingested (/s)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 6,
|
||||||
|
"x": 12,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum(rate(loki_distributor_lines_received_total[5m]))",
|
||||||
|
"legendFormat": "lines/s"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"decimals": 1,
|
||||||
|
"color": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Spans ingested (/s)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 6,
|
||||||
|
"x": 18,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum(rate(tempo_distributor_spans_received_total[5m]))",
|
||||||
|
"legendFormat": "spans/s"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"decimals": 1,
|
||||||
|
"color": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "purple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "row",
|
||||||
|
"title": "Stack resources",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 6
|
||||||
|
},
|
||||||
|
"collapsed": false,
|
||||||
|
"panels": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "CPU per component (observability namespace)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 7
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum by (pod) (rate(container_cpu_usage_seconds_total{namespace=\"observability\", container!=\"\"}[$__rate_interval]))",
|
||||||
|
"legendFormat": "{{pod}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"custom": {
|
||||||
|
"drawStyle": "line",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"showPoints": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"calcs": [
|
||||||
|
"mean",
|
||||||
|
"max"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Memory per component (observability namespace)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 7
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum by (pod) (container_memory_working_set_bytes{namespace=\"observability\", container!=\"\"})",
|
||||||
|
"legendFormat": "{{pod}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "bytes",
|
||||||
|
"custom": {
|
||||||
|
"drawStyle": "line",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"fillOpacity": 10,
|
||||||
|
"showPoints": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"calcs": [
|
||||||
|
"mean",
|
||||||
|
"max"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "row",
|
||||||
|
"title": "Logs",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 15
|
||||||
|
},
|
||||||
|
"collapsed": false,
|
||||||
|
"panels": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "logs",
|
||||||
|
"title": "Recent stack errors",
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "loki"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "loki"
|
||||||
|
},
|
||||||
|
"expr": "{namespace=\"observability\"} |~ \"(?i)(error|level=error|fatal)\"",
|
||||||
|
"queryType": "range"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"showTime": true,
|
||||||
|
"showLabels": false,
|
||||||
|
"wrapLogMessage": true,
|
||||||
|
"sortOrder": "Descending",
|
||||||
|
"dedupStrategy": "none",
|
||||||
|
"enableLogDetails": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/component: ui
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: grafana
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: grafana
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/component: ui
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "3000"
|
||||||
|
prometheus.io/path: "/metrics"
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 472
|
||||||
|
runAsGroup: 472
|
||||||
|
fsGroup: 472
|
||||||
|
containers:
|
||||||
|
- name: grafana
|
||||||
|
image: grafana/grafana:13.2.0
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3000
|
||||||
|
env:
|
||||||
|
- name: GF_SECURITY_ADMIN_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: observability-secrets
|
||||||
|
key: GF_SECURITY_ADMIN_USER
|
||||||
|
- name: GF_SECURITY_ADMIN_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: observability-secrets
|
||||||
|
key: GF_SECURITY_ADMIN_PASSWORD
|
||||||
|
- name: GF_SERVER_ROOT_URL
|
||||||
|
value: https://grafana.sttlab.local
|
||||||
|
- name: GF_PATHS_PROVISIONING
|
||||||
|
value: /etc/grafana/provisioning
|
||||||
|
# Everything is driven by the provisioning files, so creating
|
||||||
|
# dashboards or datasources from the UI is disabled.
|
||||||
|
- name: GF_USERS_ALLOW_SIGN_UP
|
||||||
|
value: "false"
|
||||||
|
- name: GF_ANALYTICS_REPORTING_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: GF_ANALYTICS_CHECK_FOR_UPDATES
|
||||||
|
value: "false"
|
||||||
|
- name: GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES
|
||||||
|
value: "false"
|
||||||
|
- name: GF_NEWS_NEWS_FEED_ENABLED
|
||||||
|
value: "false"
|
||||||
|
- name: GF_LOG_LEVEL
|
||||||
|
value: info
|
||||||
|
- name: GF_FEATURE_TOGGLES_ENABLE
|
||||||
|
value: traceqlEditor,traceQLStreaming,correlations,lokiLogsDataplane
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/grafana
|
||||||
|
- name: provisioning-datasources
|
||||||
|
mountPath: /etc/grafana/provisioning/datasources
|
||||||
|
- name: provisioning-dashboards
|
||||||
|
mountPath: /etc/grafana/provisioning/dashboards
|
||||||
|
- name: dashboards
|
||||||
|
mountPath: /var/lib/grafana/dashboards
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: grafana-data
|
||||||
|
- name: provisioning-datasources
|
||||||
|
configMap:
|
||||||
|
name: grafana-datasources
|
||||||
|
- name: provisioning-dashboards
|
||||||
|
configMap:
|
||||||
|
name: grafana-dashboard-providers
|
||||||
|
- name: dashboards
|
||||||
|
# Stable-named ConfigMap: updated in place by `make dashboards` and
|
||||||
|
# re-read by Grafana without a restart (~70s: kubelet sync + 10s poll).
|
||||||
|
configMap:
|
||||||
|
name: grafana-dashboards
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- pvc.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
# Dashboards live in ./dashboards and are referenced by the root kustomization:
|
||||||
|
# `k8s/grafana` is the runtime alone, `k8s/grafana/dashboards` the content alone.
|
||||||
|
# Both are deployable independently.
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
# Name-suffix hash KEPT here: datasource and provider provisioning is only
|
||||||
|
# read at startup, so the hash deliberately forces a pod restart whenever
|
||||||
|
# these files change.
|
||||||
|
- name: grafana-datasources
|
||||||
|
files:
|
||||||
|
- datasources.yaml=provisioning/datasources.yaml
|
||||||
|
- name: grafana-dashboard-providers
|
||||||
|
files:
|
||||||
|
- dashboard-providers.yaml=provisioning/dashboard-providers.yaml
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# File-based dashboard provider.
|
||||||
|
# The JSON files are mounted from the `grafana-dashboards` ConfigMap (generated
|
||||||
|
# without a name-suffix hash): `make dashboards` updates the ConfigMap in place
|
||||||
|
# and Grafana reloads WITHOUT restarting the pod, in about 70s
|
||||||
|
# (kubelet volume sync ~60s + updateIntervalSeconds below).
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: infra-observability
|
||||||
|
orgId: 1
|
||||||
|
folder: Infra Observability
|
||||||
|
folderUid: infra-observability
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
allowUiUpdates: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
|
foldersFromFilesStructure: false
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Declarative datasource provisioning.
|
||||||
|
# The `uid` values are STABLE and are what dashboard JSON files reference:
|
||||||
|
# "datasource": { "type": "prometheus", "uid": "prometheus" }
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
deleteDatasources:
|
||||||
|
- name: Prometheus
|
||||||
|
orgId: 1
|
||||||
|
- name: Loki
|
||||||
|
orgId: 1
|
||||||
|
- name: Tempo
|
||||||
|
orgId: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
uid: prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
httpMethod: POST
|
||||||
|
timeInterval: 30s
|
||||||
|
manageAlerts: false
|
||||||
|
exemplarTraceIdDestinations:
|
||||||
|
- name: trace_id
|
||||||
|
datasourceUid: tempo
|
||||||
|
|
||||||
|
- name: Loki
|
||||||
|
uid: loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://loki:3100
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
timeout: 60
|
||||||
|
maxLines: 1000
|
||||||
|
# Logs -> traces: a trace_id in the log line opens Tempo.
|
||||||
|
derivedFields:
|
||||||
|
- name: TraceID
|
||||||
|
matcherType: regex
|
||||||
|
matcherRegex: '"?trace[_-]?id"?[:=]"?([a-f0-9]{16,32})'
|
||||||
|
url: '$${__value.raw}'
|
||||||
|
datasourceUid: tempo
|
||||||
|
urlDisplayLabel: View trace
|
||||||
|
|
||||||
|
- name: Tempo
|
||||||
|
uid: tempo
|
||||||
|
type: tempo
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://tempo:3200
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
httpMethod: GET
|
||||||
|
streamingEnabled:
|
||||||
|
search: true
|
||||||
|
metrics: true
|
||||||
|
# Service graph fed by the metrics_generator -> Prometheus.
|
||||||
|
serviceMap:
|
||||||
|
datasourceUid: prometheus
|
||||||
|
nodeGraph:
|
||||||
|
enabled: true
|
||||||
|
tracesToLogsV2:
|
||||||
|
datasourceUid: loki
|
||||||
|
spanStartTimeShift: -5m
|
||||||
|
spanEndTimeShift: 5m
|
||||||
|
filterByTraceID: true
|
||||||
|
filterBySpanID: false
|
||||||
|
tags:
|
||||||
|
- key: service.name
|
||||||
|
value: app
|
||||||
|
tracesToMetrics:
|
||||||
|
datasourceUid: prometheus
|
||||||
|
spanStartTimeShift: -5m
|
||||||
|
spanEndTimeShift: 5m
|
||||||
|
tags:
|
||||||
|
- key: service.name
|
||||||
|
value: service
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: grafana-data
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/component: storage
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/component: ui
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: grafana
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3000
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Certificates issued by the `sttlab-local-ca` cert-manager ClusterIssuer
|
||||||
|
# (already present on the cluster). No private key is ever versioned.
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: observability-tls
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
secretName: observability-tls
|
||||||
|
issuerRef:
|
||||||
|
name: sttlab-local-ca
|
||||||
|
kind: ClusterIssuer
|
||||||
|
commonName: grafana.sttlab.local
|
||||||
|
dnsNames:
|
||||||
|
- grafana.sttlab.local
|
||||||
|
- prometheus.sttlab.local
|
||||||
|
- alloy.sttlab.local
|
||||||
|
- loki.sttlab.local
|
||||||
|
- tempo.sttlab.local
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: grafana
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||||
|
# Required for TraceQL streaming and Loki live tail.
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- grafana.sttlab.local
|
||||||
|
secretName: observability-tls
|
||||||
|
rules:
|
||||||
|
- host: grafana.sttlab.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: grafana
|
||||||
|
port:
|
||||||
|
number: 3000
|
||||||
|
---
|
||||||
|
# Exposed for diagnostics (targets, TSDB status, expression browser).
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- prometheus.sttlab.local
|
||||||
|
secretName: observability-tls
|
||||||
|
rules:
|
||||||
|
- host: prometheus.sttlab.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: prometheus
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
|
---
|
||||||
|
# Alloy UI: state of the log collection pipeline components.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: alloy
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: alloy
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- alloy.sttlab.local
|
||||||
|
secretName: observability-tls
|
||||||
|
rules:
|
||||||
|
- host: alloy.sttlab.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: alloy
|
||||||
|
port:
|
||||||
|
number: 12345
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- certificate.yaml
|
||||||
|
- ingress.yaml
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- secret.yaml
|
||||||
|
- prometheus
|
||||||
|
- loki
|
||||||
|
- tempo
|
||||||
|
- alloy
|
||||||
|
- grafana
|
||||||
|
- grafana/dashboards
|
||||||
|
- ingress
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- includeSelectors: false
|
||||||
|
pairs:
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: loki
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: loki
|
||||||
|
app.kubernetes.io/component: logs
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: loki
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: loki
|
||||||
|
app.kubernetes.io/name: loki
|
||||||
|
app.kubernetes.io/component: logs
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "3100"
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
fsGroup: 10001
|
||||||
|
containers:
|
||||||
|
- name: loki
|
||||||
|
image: grafana/loki:3.7.6
|
||||||
|
args:
|
||||||
|
- -config.file=/etc/loki/loki.yaml
|
||||||
|
- -target=all
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3100
|
||||||
|
- name: grpc
|
||||||
|
containerPort: 9096
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: 1Gi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/loki
|
||||||
|
- name: data
|
||||||
|
mountPath: /loki
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: loki-config
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: loki-data
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- pvc.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: loki-config
|
||||||
|
files:
|
||||||
|
- loki.yaml
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: loki
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Loki 3.x in monolithic mode (-target=all), filesystem storage on a PVC.
|
||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_address: 0.0.0.0
|
||||||
|
http_listen_port: 3100
|
||||||
|
grpc_listen_address: 0.0.0.0
|
||||||
|
grpc_listen_port: 9096
|
||||||
|
log_level: info
|
||||||
|
grpc_server_max_concurrent_streams: 1000
|
||||||
|
|
||||||
|
common:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
path_prefix: /loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /loki/chunks
|
||||||
|
rules_directory: /loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: "2024-01-01"
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
storage_config:
|
||||||
|
tsdb_shipper:
|
||||||
|
active_index_directory: /loki/tsdb-index
|
||||||
|
cache_location: /loki/tsdb-cache
|
||||||
|
filesystem:
|
||||||
|
directory: /loki/chunks
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
working_directory: /loki/compactor
|
||||||
|
retention_enabled: true
|
||||||
|
delete_request_store: filesystem
|
||||||
|
compaction_interval: 10m
|
||||||
|
retention_delete_delay: 2h
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
retention_period: 168h
|
||||||
|
max_query_series: 5000
|
||||||
|
ingestion_rate_mb: 16
|
||||||
|
ingestion_burst_size_mb: 32
|
||||||
|
reject_old_samples: true
|
||||||
|
reject_old_samples_max_age: 168h
|
||||||
|
volume_enabled: true
|
||||||
|
metric_aggregation_enabled: true
|
||||||
|
allow_structured_metadata: true
|
||||||
|
|
||||||
|
pattern_ingester:
|
||||||
|
enabled: true
|
||||||
|
metric_aggregation:
|
||||||
|
loki_address: localhost:3100
|
||||||
|
|
||||||
|
query_range:
|
||||||
|
align_queries_with_step: true
|
||||||
|
cache_results: true
|
||||||
|
results_cache:
|
||||||
|
cache:
|
||||||
|
embedded_cache:
|
||||||
|
enabled: true
|
||||||
|
max_size_mb: 100
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
encoding: protobuf
|
||||||
|
|
||||||
|
ruler:
|
||||||
|
storage:
|
||||||
|
type: local
|
||||||
|
local:
|
||||||
|
directory: /loki/rules
|
||||||
|
rule_path: /loki/rules-tmp
|
||||||
|
enable_api: true
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
reporting_enabled: false
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: loki-data
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: loki
|
||||||
|
app.kubernetes.io/component: storage
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: loki
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: loki
|
||||||
|
app.kubernetes.io/component: logs
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: loki
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3100
|
||||||
|
targetPort: http
|
||||||
|
- name: grpc
|
||||||
|
port: 9096
|
||||||
|
targetPort: grpc
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/component: metrics
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
# ReadWriteOnce PVC: the old pod must release the volume first.
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: prometheus
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: prometheus
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/component: metrics
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "9090"
|
||||||
|
spec:
|
||||||
|
serviceAccountName: prometheus
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 65534
|
||||||
|
runAsGroup: 65534
|
||||||
|
fsGroup: 65534
|
||||||
|
containers:
|
||||||
|
- name: prometheus
|
||||||
|
image: prom/prometheus:v3.14.0
|
||||||
|
args:
|
||||||
|
- --config.file=/etc/prometheus/prometheus.yaml
|
||||||
|
- --storage.tsdb.path=/prometheus
|
||||||
|
- --storage.tsdb.retention.time=15d
|
||||||
|
- --storage.tsdb.retention.size=8GB
|
||||||
|
- --web.enable-lifecycle
|
||||||
|
# Receives remote_write from Tempo's metrics_generator (span metrics + service graphs).
|
||||||
|
- --web.enable-remote-write-receiver
|
||||||
|
- --web.external-url=https://prometheus.sttlab.local
|
||||||
|
- --web.route-prefix=/
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 9090
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: 2Gi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /-/ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /-/healthy
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/prometheus
|
||||||
|
- name: data
|
||||||
|
mountPath: /prometheus
|
||||||
|
- name: librechat-metrics
|
||||||
|
mountPath: /etc/prometheus/secrets/librechat
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: prometheus-config
|
||||||
|
- name: librechat-metrics
|
||||||
|
secret:
|
||||||
|
secretName: librechat-metrics
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: prometheus-data
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- secret.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: prometheus-config
|
||||||
|
files:
|
||||||
|
- prometheus.yaml
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 30s
|
||||||
|
evaluation_interval: 30s
|
||||||
|
external_labels:
|
||||||
|
cluster: orbstack
|
||||||
|
source: prometheus
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
# --- Control plane -------------------------------------------------------
|
||||||
|
- job_name: kubernetes-apiservers
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: endpoints
|
||||||
|
scheme: https
|
||||||
|
tls_config:
|
||||||
|
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
insecure_skip_verify: true
|
||||||
|
authorization:
|
||||||
|
credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels:
|
||||||
|
[__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
|
||||||
|
action: keep
|
||||||
|
regex: default;kubernetes;https
|
||||||
|
|
||||||
|
# --- Kubelet -------------------------------------------------------------
|
||||||
|
- job_name: kubernetes-nodes
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: node
|
||||||
|
scheme: https
|
||||||
|
tls_config:
|
||||||
|
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
insecure_skip_verify: true
|
||||||
|
authorization:
|
||||||
|
credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||||
|
relabel_configs:
|
||||||
|
- action: labelmap
|
||||||
|
regex: __meta_kubernetes_node_label_(.+)
|
||||||
|
- target_label: __address__
|
||||||
|
replacement: kubernetes.default.svc:443
|
||||||
|
- source_labels: [__meta_kubernetes_node_name]
|
||||||
|
regex: (.+)
|
||||||
|
target_label: __metrics_path__
|
||||||
|
replacement: /api/v1/nodes/$1/proxy/metrics
|
||||||
|
|
||||||
|
# --- Container resource metrics -----------------------------------------
|
||||||
|
# /metrics/resource and NOT /metrics/cadvisor: on this cluster (OrbStack) the
|
||||||
|
# kubelet's cadvisor endpoint only exposes `machine_*` series and no
|
||||||
|
# `container_*` at all. /metrics/resource -- the source metrics-server reads --
|
||||||
|
# provides container_cpu_usage_seconds_total and
|
||||||
|
# container_memory_working_set_bytes with namespace/pod/container labels.
|
||||||
|
- job_name: kubernetes-kubelet-resource
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: node
|
||||||
|
scheme: https
|
||||||
|
tls_config:
|
||||||
|
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
insecure_skip_verify: true
|
||||||
|
authorization:
|
||||||
|
credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||||
|
relabel_configs:
|
||||||
|
- action: labelmap
|
||||||
|
regex: __meta_kubernetes_node_label_(.+)
|
||||||
|
- target_label: __address__
|
||||||
|
replacement: kubernetes.default.svc:443
|
||||||
|
- source_labels: [__meta_kubernetes_node_name]
|
||||||
|
regex: (.+)
|
||||||
|
target_label: __metrics_path__
|
||||||
|
replacement: /api/v1/nodes/$1/proxy/metrics/resource
|
||||||
|
- source_labels: [__meta_kubernetes_node_name]
|
||||||
|
target_label: node
|
||||||
|
|
||||||
|
# --- Services annotated with prometheus.io/scrape -----------------------
|
||||||
|
- job_name: kubernetes-service-endpoints
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: endpoints
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
|
||||||
|
action: keep
|
||||||
|
regex: "true"
|
||||||
|
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
|
||||||
|
action: replace
|
||||||
|
target_label: __scheme__
|
||||||
|
regex: (https?)
|
||||||
|
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
|
||||||
|
action: replace
|
||||||
|
target_label: __metrics_path__
|
||||||
|
regex: (.+)
|
||||||
|
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
|
||||||
|
action: replace
|
||||||
|
target_label: __address__
|
||||||
|
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||||
|
replacement: $1:$2
|
||||||
|
- action: labelmap
|
||||||
|
regex: __meta_kubernetes_service_label_(.+)
|
||||||
|
- source_labels: [__meta_kubernetes_namespace]
|
||||||
|
target_label: namespace
|
||||||
|
- source_labels: [__meta_kubernetes_service_name]
|
||||||
|
target_label: service
|
||||||
|
- source_labels: [__meta_kubernetes_pod_name]
|
||||||
|
target_label: pod
|
||||||
|
- source_labels: [__meta_kubernetes_pod_node_name]
|
||||||
|
target_label: node
|
||||||
|
|
||||||
|
# --- Pods annotated with prometheus.io/scrape ---------------------------
|
||||||
|
- job_name: kubernetes-pods
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: pod
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
|
||||||
|
action: keep
|
||||||
|
regex: "true"
|
||||||
|
# A pod yields one target per container: without this filter the init
|
||||||
|
# containers duplicate the real target.
|
||||||
|
- source_labels: [__meta_kubernetes_pod_container_init]
|
||||||
|
action: drop
|
||||||
|
regex: "true"
|
||||||
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scheme]
|
||||||
|
action: replace
|
||||||
|
target_label: __scheme__
|
||||||
|
regex: (https?)
|
||||||
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
|
||||||
|
action: replace
|
||||||
|
target_label: __metrics_path__
|
||||||
|
regex: (.+)
|
||||||
|
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
||||||
|
action: replace
|
||||||
|
target_label: __address__
|
||||||
|
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||||
|
replacement: $1:$2
|
||||||
|
- action: labelmap
|
||||||
|
regex: __meta_kubernetes_pod_label_(.+)
|
||||||
|
- source_labels: [__meta_kubernetes_namespace]
|
||||||
|
target_label: namespace
|
||||||
|
- source_labels: [__meta_kubernetes_pod_name]
|
||||||
|
target_label: pod
|
||||||
|
- source_labels: [__meta_kubernetes_pod_node_name]
|
||||||
|
target_label: node
|
||||||
|
- source_labels: [__meta_kubernetes_pod_container_name]
|
||||||
|
target_label: container
|
||||||
|
- source_labels: [__meta_kubernetes_pod_phase]
|
||||||
|
action: drop
|
||||||
|
regex: (Succeeded|Failed|Pending)
|
||||||
|
|
||||||
|
# --- LibreChat: /metrics behind a Bearer token --------------------------
|
||||||
|
# Annotation-based discovery cannot carry credentials, so LibreChat gets its
|
||||||
|
# own job. The token comes from the `librechat-metrics` secret mounted into
|
||||||
|
# the Prometheus pod and must match METRICS_SECRET on the LibreChat side.
|
||||||
|
- job_name: librechat
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: pod
|
||||||
|
namespaces:
|
||||||
|
names: [librechat]
|
||||||
|
authorization:
|
||||||
|
type: Bearer
|
||||||
|
credentials_file: /etc/prometheus/secrets/librechat/token
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape_with_auth]
|
||||||
|
action: keep
|
||||||
|
regex: "true"
|
||||||
|
# A pod yields one target per container: without this filter the init
|
||||||
|
# containers duplicate the real target.
|
||||||
|
- source_labels: [__meta_kubernetes_pod_container_init]
|
||||||
|
action: drop
|
||||||
|
regex: "true"
|
||||||
|
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
||||||
|
action: replace
|
||||||
|
target_label: __address__
|
||||||
|
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||||
|
replacement: $1:$2
|
||||||
|
- source_labels: [__meta_kubernetes_namespace]
|
||||||
|
target_label: namespace
|
||||||
|
- source_labels: [__meta_kubernetes_pod_name]
|
||||||
|
target_label: pod
|
||||||
|
- source_labels: [__meta_kubernetes_pod_container_name]
|
||||||
|
target_label: container
|
||||||
|
- target_label: service
|
||||||
|
replacement: librechat
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: prometheus-data
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/component: storage
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: observability-prometheus
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
- nodes/metrics
|
||||||
|
- nodes/proxy
|
||||||
|
- services
|
||||||
|
- endpoints
|
||||||
|
- pods
|
||||||
|
- namespaces
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["discovery.k8s.io"]
|
||||||
|
resources: ["endpointslices"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: ["networking.k8s.io"]
|
||||||
|
resources: ["ingresses"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- nonResourceURLs: ["/metrics", "/metrics/cadvisor"]
|
||||||
|
verbs: ["get"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: observability-prometheus
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: observability-prometheus
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: prometheus
|
||||||
|
namespace: observability
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Template. The token must be IDENTICAL to the METRICS_SECRET key of the
|
||||||
|
# `librechat-secrets` secret in the librechat namespace (infra-librechat repo):
|
||||||
|
# it is the Bearer token Prometheus uses to scrape LibreChat's /metrics.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: librechat-metrics
|
||||||
|
namespace: observability
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
token: <CHANGE_ME>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: prometheus
|
||||||
|
app.kubernetes.io/component: metrics
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: prometheus
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 9090
|
||||||
|
targetPort: http
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Template: copy to secret.yaml (gitignored) and fill in the values.
|
||||||
|
# make secret
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: observability-secrets
|
||||||
|
namespace: observability
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
GF_SECURITY_ADMIN_USER: admin
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD: <CHANGE_ME>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: tempo
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tempo
|
||||||
|
app.kubernetes.io/component: traces
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: tempo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: tempo
|
||||||
|
app.kubernetes.io/name: tempo
|
||||||
|
app.kubernetes.io/component: traces
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/port: "3200"
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
fsGroup: 10001
|
||||||
|
containers:
|
||||||
|
- name: tempo
|
||||||
|
image: grafana/tempo:3.0.3
|
||||||
|
args:
|
||||||
|
- -config.file=/etc/tempo/tempo.yaml
|
||||||
|
- -target=all
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3200
|
||||||
|
- name: grpc
|
||||||
|
containerPort: 9095
|
||||||
|
- name: otlp-grpc
|
||||||
|
containerPort: 4317
|
||||||
|
- name: otlp-http
|
||||||
|
containerPort: 4318
|
||||||
|
- name: zipkin
|
||||||
|
containerPort: 9411
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: "2"
|
||||||
|
memory: 2Gi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/tempo
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/tempo
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: tempo-config
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: tempo-data
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- pvc.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: tempo-config
|
||||||
|
files:
|
||||||
|
- tempo.yaml
|
||||||
|
|
||||||
|
generatorOptions:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tempo
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: tempo-data
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tempo
|
||||||
|
app.kubernetes.io/component: storage
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: tempo
|
||||||
|
namespace: observability
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: tempo
|
||||||
|
app.kubernetes.io/component: traces
|
||||||
|
app.kubernetes.io/part-of: observability
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: tempo
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3200
|
||||||
|
targetPort: http
|
||||||
|
- name: grpc
|
||||||
|
port: 9095
|
||||||
|
targetPort: grpc
|
||||||
|
- name: otlp-grpc
|
||||||
|
port: 4317
|
||||||
|
targetPort: otlp-grpc
|
||||||
|
- name: otlp-http
|
||||||
|
port: 4318
|
||||||
|
targetPort: otlp-http
|
||||||
|
- name: zipkin
|
||||||
|
port: 9411
|
||||||
|
targetPort: zipkin
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Tempo 3.x in monolithic mode (-target=all), local storage on a PVC.
|
||||||
|
stream_over_http_enabled: true
|
||||||
|
multitenancy_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_address: 0.0.0.0
|
||||||
|
http_listen_port: 3200
|
||||||
|
grpc_listen_address: 0.0.0.0
|
||||||
|
grpc_listen_port: 9095
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
distributor:
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
zipkin:
|
||||||
|
endpoint: 0.0.0.0:9411
|
||||||
|
|
||||||
|
# Generates span metrics and service graphs, pushed to Prometheus through its
|
||||||
|
# remote-write receiver (--web.enable-remote-write-receiver).
|
||||||
|
metrics_generator:
|
||||||
|
registry:
|
||||||
|
external_labels:
|
||||||
|
source: tempo
|
||||||
|
cluster: orbstack
|
||||||
|
storage:
|
||||||
|
path: /var/tempo/generator/wal
|
||||||
|
remote_write:
|
||||||
|
- url: http://prometheus:9090/api/v1/write
|
||||||
|
send_exemplars: true
|
||||||
|
|
||||||
|
query_frontend:
|
||||||
|
search:
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1073741824
|
||||||
|
trace_by_id:
|
||||||
|
duration_slo: 5s
|
||||||
|
|
||||||
|
storage:
|
||||||
|
trace:
|
||||||
|
backend: local
|
||||||
|
wal:
|
||||||
|
path: /var/tempo/wal
|
||||||
|
local:
|
||||||
|
path: /var/tempo/blocks
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
defaults:
|
||||||
|
metrics_generator:
|
||||||
|
processors:
|
||||||
|
- service-graphs
|
||||||
|
- span-metrics
|
||||||
|
generate_native_histograms: both
|
||||||
|
# Tempo 3.x: retention is no longer a top-level `compactor:` block; it is
|
||||||
|
# driven from here (backend_scheduler/backend_worker otherwise).
|
||||||
|
compaction:
|
||||||
|
block_retention: 168h
|
||||||
|
|
||||||
|
usage_report:
|
||||||
|
reporting_enabled: false
|
||||||
Reference in New Issue
Block a user