Skip to main content
Version: 5.1

Kubernetes Production Stack

Enterprise-grade observability setup for Kubernetes deployments using Prometheus Operator, Grafana, and Jaeger. Includes automatic metric scraping, distributed tracing, and managed dashboards.

Architecture​

WSO2 Integrator Pods
├── Metrics (9797) ──▶ Prometheus Operator (auto-discovers ServiceMonitor)
│ ▼
│ Prometheus StatefulSet
│ ▼
└─ Traces (4317) ──▶ Jaeger Deployment (OTLP receiver)
▼
Elasticsearch

Grafana Deployment ◀── Prometheus, Jaeger

Prerequisites​

  • Kubernetes cluster 1.25+
  • Helm 3+
  • kubectl configured

Installation Steps​

Step 1: Create Observability Namespace​

kubectl create namespace observability

Step 2: Add Helm Repositories​

# Prometheus Operator
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# Grafana
helm repo add grafana https://grafana.github.io/helm-charts

# Jaeger
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts

helm repo update

Step 3: Install Prometheus Stack​

Create prometheus-values.yml:

prometheus:
prometheusSpec:
retention: 15d
enableAdminAPI: true
resources:
requests:
cpu: 500m
memory: 2Gi
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi

grafana:
enabled: true
adminPassword: "admin-password"

alertmanager:
enabled: true
config:
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 10s
group_interval: 10s
repeat_interval: 12h
receiver: 'null'

The chart provisions Grafana's Prometheus data source, so you don't need to add one.

helm install prometheus prometheus-community/kube-prometheus-stack \
-n observability \
-f prometheus-values.yml

Step 4: Install Jaeger​

Create jaeger-values.yml. It configures Jaeger v2 to receive OTLP traces and store them in Elasticsearch. Replace http://elasticsearch:9200 with the URL of your Elasticsearch cluster.

userconfig:
service:
extensions: [jaeger_storage, jaeger_query, healthcheckv2]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter]
extensions:
healthcheckv2:
use_v2: true
http:
endpoint: 0.0.0.0:13133
jaeger_query:
storage:
traces: primary_store
jaeger_storage:
backends:
primary_store:
elasticsearch:
server_urls: ["http://elasticsearch:9200"]
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
exporters:
jaeger_storage_exporter:
trace_storage: primary_store
helm install jaeger jaegertracing/jaeger \
-n observability \
-f jaeger-values.yml

Step 5: Deploy Your Integration​

Replace myorg/wso2-integrator:latest with your integration's container image. The integration must import ballerinax/prometheus and ballerinax/jaeger and be built with observabilityIncluded = true, as described in Metrics and Jaeger distributed tracing. The environment variables below enable metrics and tracing without a Config.toml.

apiVersion: apps/v1
kind: Deployment
metadata:
name: wso2-integrator
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: wso2-integrator
template:
metadata:
labels:
app: wso2-integrator
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9797"
prometheus.io/path: "/metrics"
spec:
containers:
- name: integrator
image: myorg/wso2-integrator:latest
ports:
- name: http
containerPort: 9090
- name: metrics
containerPort: 9797
env:
- name: BAL_CONFIG_VAR_BALLERINA_OBSERVE_METRICSENABLED
value: "true"
- name: BAL_CONFIG_VAR_BALLERINA_OBSERVE_METRICSREPORTER
value: "prometheus"
- name: BAL_CONFIG_VAR_BALLERINAX_PROMETHEUS_PORT
value: "9797"
- name: BAL_CONFIG_VAR_BALLERINA_OBSERVE_TRACINGENABLED
value: "true"
- name: BAL_CONFIG_VAR_BALLERINA_OBSERVE_TRACINGPROVIDER
value: "jaeger"
- name: BAL_CONFIG_VAR_BALLERINAX_JAEGER_AGENTHOSTNAME
value: "jaeger.observability.svc.cluster.local"
- name: BAL_CONFIG_VAR_BALLERINAX_JAEGER_AGENTPORT
value: "4317"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 10
periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
name: wso2-integrator
namespace: default
labels:
app: wso2-integrator
spec:
type: ClusterIP
ports:
- name: http
port: 9090
targetPort: 9090
- name: metrics
port: 9797
targetPort: 9797
selector:
app: wso2-integrator

Step 6: Create ServiceMonitor for Prometheus​

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: wso2-integrator
namespace: default
labels:
release: prometheus
spec:
selector:
matchLabels:
app: wso2-integrator
endpoints:
- port: metrics
path: /metrics
interval: 30s

Verifying the Setup​

Check Prometheus Targets​

kubectl port-forward -n observability svc/prometheus-operated 9090:9090
# Visit http://localhost:9090/targets

Check Grafana​

kubectl port-forward -n observability svc/prometheus-grafana 3000:80
# Visit http://localhost:3000 (admin / admin-password)

Check Jaeger UI​

kubectl port-forward -n observability svc/jaeger 16686:16686
# Visit http://localhost:16686

Creating Alert Rules​

Create a PrometheusRule for alerting:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: wso2-integrator-alerts
namespace: default
labels:
release: prometheus
spec:
groups:
- name: wso2-integrator.rules
interval: 30s
rules:
- alert: HighErrorRate
expr: |
(sum(rate(requests_total_value{http_status_code_group="5xx"}[5m])) by (service)) /
(sum(rate(requests_total_value[5m])) by (service)) > 0.05
for: 5m
labels:
severity: critical
service: "{{ $labels.service }}"
annotations:
summary: "High error rate on {{ $labels.service }}"
description: "Error rate is {{ $value | humanizePercentage }}"

- alert: HighLatency
expr: |
max(response_time_seconds{quantile="0.95", timeWindow="300000"}) by (service) > 2
for: 10m
labels:
severity: warning
service: "{{ $labels.service }}"
annotations:
summary: "High P95 latency on {{ $labels.service }}"
description: "P95 latency is {{ $value }}s"

- alert: PodCrashLooping
expr: |
rate(kube_pod_container_status_restarts_total{pod=~"wso2-integrator.*"}[15m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"

Creating Grafana Dashboards​

Import pre-built dashboard for Kubernetes:

# Download the dashboard and point it at the Prometheus data source
curl -s https://grafana.com/api/dashboards/7249/revisions/1/download \
| sed 's/${DS_PROMETHEUS}/Prometheus/g' > kubernetes.json

# Install dashboard configmap
kubectl create configmap grafana-dashboard-kubernetes -n observability \
--from-file=kubernetes.json
kubectl label configmap grafana-dashboard-kubernetes -n observability \
grafana_dashboard=1

Grafana's dashboard sidecar loads ConfigMaps that have the grafana_dashboard=1 label.

Scaling Considerations​

  • Prometheus Storage: Use persistent volumes sized for your retention period
  • Grafana: Stateless, can scale replicas
  • Jaeger: Uses Elasticsearch backend; ensure sufficient storage
  • Retention: Set appropriate retention for logs and metrics

Backup and Recovery​

Backup Prometheus Data​

Take a TSDB snapshot through the admin API (enabled by enableAdminAPI: true in Step 3):

kubectl port-forward -n observability svc/prometheus-operated 9090:9090
# In another terminal:
curl -X POST http://localhost:9090/api/v1/admin/tsdb/snapshot

Prometheus writes the snapshot to the snapshots directory on its data volume. Back up that volume with your volume snapshot or backup tool.

Backup Grafana Dashboards​

Export dashboards through the Grafana HTTP API:

kubectl port-forward -n observability svc/prometheus-grafana 3000:80
# In another terminal, list the dashboards and note the uid of each one:
curl -s -u admin:admin-password "http://localhost:3000/api/search?type=dash-db"
curl -s -u admin:admin-password "http://localhost:3000/api/dashboards/uid/<uid>" \
> grafana-dashboard-backup.json

Troubleshooting​

Prometheus not scraping metrics:

# Check ServiceMonitor:
kubectl get servicemonitor -A

# Check Prometheus targets:
kubectl port-forward -n observability svc/prometheus-operated 9090:9090
# Visit http://localhost:9090/targets

Jaeger not receiving traces:

# Check Jaeger:
kubectl get pod -n observability -l app.kubernetes.io/name=jaeger

# Check logs:
kubectl logs -n observability -l app.kubernetes.io/name=jaeger

Grafana not showing metrics:

# Check that Prometheus is reachable inside the cluster:
kubectl run -n observability prometheus-check --rm -i --restart=Never \
--image=curlimages/curl -- curl -s http://prometheus-operated:9090/-/healthy

What's Next​