Kubernetes
One agent per node, as a DaemonSet: every container's logs, kubelet and node metrics, and an OTLP endpoint for your applications — named by pod annotations.
The agent runs on every node, control plane included. It reads the container log files the kubelet writes, asks the kubelet for node, pod and container metrics, and receives traces, metrics and logs your applications send with an OpenTelemetry SDK. Everything is tagged with the pod, namespace, node and workload it came from, and named after the pod's zharp.io annotations.
Install with Helm
- 1Get an API key
Create one under Integrations & API → Main API keys with the Ingest only scope. One key serves every node of every cluster.
- 2Install the chart
The chart is in the agent repository.
environmentbecomesdeployment.environment.nameon everything whose pod has nozharp.io/envannotation;clusterNameis sent ask8s.cluster.name.Helmgit clone https://github.com/zharp-io/agent.git helm install zharp-agent ./agent/deploy/helm/zharp-agent \ --namespace zharp-system --create-namespace \ --set apiKey=YOUR_API_KEY \ --set environment=production \ --set clusterName=prod-eu
To keep the key out of Helm values, create the Secret yourself and point the chart at it:
Helm, with your own Secretkubectl create namespace zharp-system kubectl -n zharp-system create secret generic zharp-agent --from-literal=api-key=YOUR_API_KEY helm install zharp-agent ./agent/deploy/helm/zharp-agent -n zharp-system \ --set existingSecret=zharp-agent
- 3Check it runs on every node
kubectl -n zharp-system get pods -l app.kubernetes.io/name=zharp-agent -o wide
Logs show up in Logs within a minute, each under its service name. Metrics follow at the next collection (every 30 seconds).
Without Helm
deploy/kubernetes holds the same objects, rendered from the chart with its defaults (zharp-agent.yaml, regenerated by render.sh). The API key comes from a Secret you create first:
kubectl create namespace zharp-system kubectl -n zharp-system create secret generic zharp-agent --from-literal=api-key=YOUR_API_KEY kubectl apply -k "github.com/zharp-io/agent/deploy/kubernetes?ref=main"
What it collects
| Source | What | Tagged with |
|---|---|---|
| Container logs | stdout and stderr of every container on the node, from /var/log/pods (containerd, CRI-O and Docker formats). Lines the runtime split are put back together, stack traces become one record, and the level is read from the line. | pod, namespace, container, node, deployment / statefulset / daemonset / job, image, stream |
| Kubelet metrics | Node, pod and container CPU, memory, network and filesystem, from the kubelet's stats API on port 10250. | the same, for pod and container metrics |
| Node metrics | The node's CPU, memory, disk, network, filesystems and load, from the host's /proc. Turn off with metrics.host=false. | host.name (the node) |
| Your applications | OTLP traces, metrics and logs on port 4317 (gRPC) and 4318 (HTTP), through a Service that keeps each pod's data on its own node. | the sending pod, found by its IP |
The agent's own pods are never read. Every record also carries service.name, deployment.environment.name, service.version and, for logs, zharp.source — see below.
Naming your workloads
Put annotations on the pod template. Without them, the service is the pod's app.kubernetes.io/name label, else the deployment, statefulset, daemonset, cronjob or job name, else the container name; the version falls back to app.kubernetes.io/version, and the source to a guess from the image (nginx, postgres, redis, eclipse-temurin, python …). An application that sets its own service.name through an OpenTelemetry SDK keeps it unless the pod is annotated.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
template:
metadata:
annotations:
zharp.io/service: checkout
zharp.io/env: production
zharp.io/version: "2.3.1"
zharp.io/source: java
# this pod's own rules, on top of the agent-wide ones
zharp.io/logs-exclude: 'GET /actuator/health'
zharp.io/logs-mask: '[{"pattern":"(card_holder=)\\w+","replace":"${1}***"}]'
spec:
containers:
- name: app
image: ghcr.io/acme/checkout:2.3.1| Annotation | Effect |
|---|---|
zharp.io/service | The service name (service.name). |
zharp.io/env | The environment (deployment.environment.name). |
zharp.io/version | The version (service.version). |
zharp.io/source | What wrote the logs, which picks how Zharp parses them (zharp.source). |
zharp.io/logs: "false" | Don't send this pod's logs. Its metrics and traces still arrive. |
zharp.io/logs-multiline | auto (default), off, or a regex matching the first line of each record. |
zharp.io/logs-exclude | A regex: matching records are dropped. |
zharp.io/logs-mask | A JSON list of {"pattern","replace"}: matches are rewritten before anything leaves the node. |
zharp.io/<container>.service | Any of the above for one container of the pod, e.g. zharp.io/proxy.logs: "false". |
The annotations are read on the node and removed before sending. Log Configuration has the multi-line rules, the rules' order and the built-in masks.
Sending traces from your apps
Point the SDK at the agent's Service. Its internalTrafficPolicy: Local sends each pod's data to the agent on the same node, which tags it with that pod and its annotations — an app that doesn't set service.name is named like its logs.
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://zharp-agent.zharp-system.svc:4318
# or, with otlp.hostPort.enabled=true, the agent on the pod's own node:
# - name: HOST_IP
# valueFrom: { fieldRef: { fieldPath: status.hostIP } }
# - name: OTEL_EXPORTER_OTLP_ENDPOINT
# value: http://$(HOST_IP):4318Chart values
| Value | Default | What |
|---|---|---|
apiKey | — | The API key; the chart stores it in a Secret. |
existingSecret | — | A Secret holding the key (under existingSecretKey, default api-key), used instead of apiKey. |
endpoint | Zharp cloud | The Zharp API URL, for self-hosted Zharp. |
environment | — | deployment.environment.name for pods without zharp.io/env. |
clusterName | — | k8s.cluster.name on everything sent. |
logs.enabled | true | Container logs. |
logs.multiline | auto | Joining for pods without zharp.io/logs-multiline. |
logs.excludeNamespaces | [] | Namespaces whose logs aren't read at all. |
metrics.enabled | true | With metrics.kubelet and metrics.host (both true) and metrics.collectionInterval (30s). |
traces.enabled | true | The OTLP receiver for your applications. |
otlp.service.enabled | true | The ClusterIP Service on 4317/4318. |
otlp.hostPort.enabled | false | Also open 4317/4318 on each node. |
rules.exclude | [] | Regexes; matching log records are dropped, in every pod. |
rules.mask | [] | List of {pattern, replace}, in every pod. |
rules.defaultMasks | true | The built-in masks; rules.disableDefaultMasks turns single ones off. |
tolerations | [{operator: Exists}] | Run on every node, tainted ones included. |
resources | 100m / 192Mi, up to 1 CPU / 512Mi | Requests and limits; the agent sheds load at 80% of the memory limit. |
image.repository | ghcr.io/zharp-io/agent | With image.tag (the chart's version) and image.pullPolicy. |
environment: production
clusterName: prod-eu
logs:
excludeNamespaces: [kube-system]
rules:
exclude:
- 'GET /healthz'
mask:
- pattern: '(password=)\S+'
replace: '${1}***'
disableDefaultMasks: [jwt]What it needs
- Read-only RBAC: get, list and watch on pods, namespaces, nodes, endpoints, replicasets, deployments, statefulsets, daemonsets, jobs and cronjobs (to name workloads and read annotations), and get on
nodes/stats,nodes/proxyandnodes/metricsfor the kubelet. - Host paths, read-only:
/var/log/pods,/var/lib/docker/containers(where Docker-runtime nodes keep the files), and/as/hostfsfor node metrics. One writable path,/var/lib/zharp-agent, remembers how far each file was read, so a restart neither repeats nor skips lines. - User: the container runs as root, because the kubelet's log files are readable only by root; it drops every capability and has a read-only root filesystem.