Grafana Alloy to Nightlamp
Alloy is Promtail's successor in the Grafana stack — same Loki push protocol, very different config syntax. This guide wires Alloy's loki.source.kubernetes component through loki.write to Nightlamp, including a Promtail → Alloy migration note for teams switching over.
Why Alloy
Grafana deprecated Promtail in 2024 in favor of Alloy. New clusters should reach for Alloy first; existing Promtail deployments work indefinitely but won't get new features. Alloy's config language is River — a typed HCL-like syntax that catches misconfigurations at startup instead of at first push.
clients[] becomes Alloy's loki.write; scrape_configs[] becomes loki.source.kubernetes plus discovery.kubernetes components. Relabeling moves from inline relabel_configs blocks to a separate discovery.relabel component. See the migration table at the bottom of this page.Prerequisites
- A Kubernetes cluster you can apply manifests to.
- A Nightlamp app registered — app ID + DSN key.
- Alloy
v1.4+(Oct 2024). Earlier preview versions used an older River dialect.
1. ServiceAccount + RBAC
rbac.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: alloy
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: alloy-read
rules:
- apiGroups: [""]
resources: ["namespaces", "nodes", "pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: alloy-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: alloy-read
subjects:
- kind: ServiceAccount
name: alloy
namespace: default2. Secret with your DSN
Create the Secret
kubectl create secret generic alloy-dsn \ --namespace=default \ --from-literal=dsn=<your-dsn-key>
3. ConfigMap (River)
The pipeline as four River components: discovery.kubernetes finds pods, discovery.relabel keeps only opted-in workloads and shapes the label set, loki.source.kubernetes tails the discovered log files, and loki.write ships them to Nightlamp with the auth headers.
configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: alloy-config
namespace: default
data:
config.alloy: |
// — Discovery —————————————————————————————————————
discovery.kubernetes "pods" {
role = "pod"
}
// Keep only pods carrying the nightlamp.app/id label, and reshape
// the labels that ride along on each line.
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_pod_label_nightlamp_app_id"]
action = "keep"
regex = ".+"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_nightlamp_app_id"]
target_label = "app"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
}
// — Source ————————————————————————————————————————
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.process.drop_health.receiver]
}
// Drop 2xx /health* probe spam.
loki.process "drop_health" {
stage.drop {
expression = "\"/health[^\"]*\" 2\\d\\d"
}
forward_to = [loki.write.nightlamp.receiver]
}
// — Sink ——————————————————————————————————————————
loki.write "nightlamp" {
endpoint {
url = "https://api.nightlamp.app/api/loki/api/v1/push"
headers = {
"X-Nightlamp-App-Id" = "<your-app-id>",
"X-Nightlamp-Dsn-Key" = env("NIGHTLAMP_DSN"),
}
}
}4. DaemonSet
Alloy's official image expects the config at /etc/alloy/config.alloy and exposes port 12345 for the local debugging UI. The DSN env var sources from the Secret above.
daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: alloy
namespace: default
spec:
selector:
matchLabels: { app: alloy }
template:
metadata:
labels: { app: alloy }
spec:
serviceAccountName: alloy
tolerations:
- operator: Exists
effect: NoSchedule
- operator: Exists
effect: NoExecute
containers:
- name: alloy
image: grafana/alloy:v1.4.2
args:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
- --storage.path=/var/lib/alloy/data
env:
- name: NIGHTLAMP_DSN
valueFrom:
secretKeyRef:
name: alloy-dsn
key: dsn
ports:
- { name: http, containerPort: 12345 }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
volumeMounts:
- { name: config, mountPath: /etc/alloy }
- { name: varlog, mountPath: /var/log, readOnly: true }
- { name: data, mountPath: /var/lib/alloy/data }
volumes:
- name: config
configMap: { name: alloy-config }
- { name: varlog, hostPath: { path: /var/log } }
- { name: data, hostPath: { path: /var/lib/alloy/data, type: DirectoryOrCreate } }5. Opt your workloads in
In each app's Deployment manifest
spec:
template:
metadata:
labels:
nightlamp.app/id: <your-app-id>6. Apply + verify
- Apply the manifests
rbac → secret → configmap → daemonset
kubectl apply -f rbac.yaml kubectl apply -f configmap.yaml kubectl apply -f daemonset.yaml
- Open Alloy's debug UI
Port-forward and browse to localhost:12345
kubectl port-forward daemonset/alloy 12345:12345
The Components tab shows live throughput per
loki.*block. A greenloki.write.nightlampmeans deliveries are succeeding; red surfaces the upstream status.
Promtail → Alloy migration table
Field-by-field mapping for teams converting an existing Promtail config. Keep the labels, the DSN, and the opt-in tag; change only the component shapes.
clients[]→loki.write "X" { endpoint { url=… headers={…} } }scrape_configs[].kubernetes_sd_configs→discovery.kubernetes "X" { role="pod" }relabel_configs[]→discovery.relabel "X" { rule { ... } }(one rule per relabel)pipeline_stages[]→loki.process "X" { stage.drop|json|labels ... }- Promtail's
__path__magic relabel is implicit inloki.source.kubernetes; you don't write it.