-
Notifications
You must be signed in to change notification settings - Fork 0
feat(observability): add a local Prometheus and Grafana stack fed by a metrics simulator #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sourcehawk
merged 11 commits into
feat/dashboards-and-alerts
from
feat/dashboards-and-alerts--dev-stack
Aug 23, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7c23cd8
feat(observability): add the simulator's controller-runtime metric lo…
sourcehawk 4316b78
feat(observability): add a metrics simulator that plays a scripted op…
sourcehawk 62f4a51
feat(observability): add a docker compose Prometheus and Grafana stac…
sourcehawk eed1bfa
chore(observability): promote client_model to a direct dependency (#185)
sourcehawk 36f1de0
fix(observability): bind the dev stack to localhost and tidy simulato…
sourcehawk d93e9ef
fix(observability): satisfy goconst in the simulator (#185)
sourcehawk 9744296
feat(observability): model persistent active workers and smoke test t…
sourcehawk 8f34678
fix(observability): harden the dev render and stack startup in the Ma…
sourcehawk 147e2b4
fix(observability): align the simulator with real controller-runtime …
sourcehawk 8a5e5c0
refactor(observability): source workqueue names from controller-runti…
sourcehawk cf209e4
fix(observability): harden dev stack startup and make the world test …
sourcehawk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Local Prometheus + Grafana for looking at the dashboards and alerts with the | ||
| # simulator's data behind them. Start with `make observability-up`. | ||
| services: | ||
| prometheus: | ||
| image: prom/prometheus:v3.14.0 | ||
| command: | ||
| - --config.file=/etc/prometheus/prometheus.yml | ||
| - --web.enable-lifecycle | ||
| ports: | ||
| - "127.0.0.1:9090:9090" | ||
| volumes: | ||
| - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro | ||
| - ../generated/dev/alerts:/etc/prometheus/rules:ro | ||
| extra_hosts: | ||
| - "host.docker.internal:host-gateway" | ||
|
|
||
| grafana: | ||
| image: grafana/grafana:13.1.4 | ||
| ports: | ||
| - "127.0.0.1:3000:3000" | ||
| environment: | ||
| GF_AUTH_ANONYMOUS_ENABLED: "true" | ||
| GF_AUTH_ANONYMOUS_ORG_ROLE: Admin | ||
| GF_AUTH_DISABLE_LOGIN_FORM: "true" | ||
| volumes: | ||
| - ./grafana/provisioning:/etc/grafana/provisioning:ro | ||
| - ../generated/dev/dashboards:/var/lib/grafana/dashboards:ro | ||
| depends_on: | ||
| - prometheus |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| apiVersion: 1 | ||
| providers: | ||
| - name: ocf | ||
| folder: OCF | ||
| type: file | ||
| disableDeletion: true | ||
| options: | ||
| path: /var/lib/grafana/dashboards |
9 changes: 9 additions & 0 deletions
9
observability/dev/grafana/provisioning/datasources/prometheus.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| apiVersion: 1 | ||
| datasources: | ||
| - name: Prometheus | ||
| type: prometheus | ||
| uid: prometheus | ||
| access: proxy | ||
| url: http://prometheus:9090 | ||
| isDefault: true | ||
| editable: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Scrapes the simulator running on the host. The static `namespace` and `pod` | ||
| # target labels collide with the namespace label the condition gauge exports, | ||
| # which Prometheus resolves by renaming the exported one to | ||
| # `exported_namespace`, exactly as a ServiceMonitor scrape does in a cluster. | ||
| global: | ||
| scrape_interval: 5s | ||
| evaluation_interval: 10s | ||
|
|
||
| rule_files: | ||
| - /etc/prometheus/rules/*.yaml | ||
|
|
||
| scrape_configs: | ||
| - job_name: demo-operator | ||
| static_configs: | ||
| - targets: ["host.docker.internal:8080"] | ||
| labels: | ||
| namespace: operators | ||
| pod: demo-operator-0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // Command simulator exposes a synthetic operator's metrics on /metrics for the | ||
| // local observability stack under observability/dev. | ||
| // | ||
| // The framework's own series (resource applies, conditions) are recorded | ||
| // through the real pkg/metrics recorder; controller-runtime, workqueue, REST | ||
| // client and leader election series are lookalikes guarded by runtime_test.go. | ||
| // The world it plays is described in docs/observability.md. | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "flag" | ||
| "fmt" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/prometheus/client_golang/prometheus/promhttp" | ||
| ocm "github.com/sourcehawk/go-crd-condition-metrics/pkg/crd-condition-metrics" | ||
|
|
||
| "github.com/sourcehawk/operator-component-framework/pkg/metrics" | ||
| ) | ||
|
|
||
| func main() { | ||
| if err := run(); err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| } | ||
|
|
||
| // run wires the registry the way an operator would, serves /metrics and plays | ||
| // the world until the process is signalled. It returns an error when the | ||
| // metrics endpoint failed to serve, so main exits non-zero and | ||
| // `make observability-up` fails visibly instead of idling without metrics. | ||
| func run() error { | ||
| listen := flag.String("listen", ":8080", "address to serve /metrics on") | ||
| metricNamespace := flag.String("metric-namespace", "demo", "metric namespace of the condition gauge") | ||
| leader := flag.Bool("leader", true, "report this replica as the leader; false shows OperatorLeaderMissing") | ||
| flag.Parse() | ||
|
|
||
| reg := prometheus.NewRegistry() | ||
| conditions := ocm.NewOperatorConditionsGauge(*metricNamespace) | ||
| collectors := metrics.NewCollectors() | ||
| reg.MustRegister(conditions, collectors) | ||
| rt := newRuntimeMetrics(reg) | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| mux := http.NewServeMux() | ||
| mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) | ||
| srv := &http.Server{Addr: *listen, Handler: mux, ReadHeaderTimeout: 5 * time.Second} | ||
| serveErr := make(chan error, 1) | ||
| go func() { | ||
| log.Printf("serving metrics on %s/metrics", *listen) | ||
| if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { | ||
| serveErr <- err | ||
| stop() | ||
| } | ||
| }() | ||
|
|
||
| newWorld(conditions, collectors, rt, *leader).run(ctx) | ||
|
|
||
| shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| _ = srv.Shutdown(shutdown) | ||
| select { | ||
| case err := <-serveErr: | ||
| return fmt.Errorf("serving metrics: %w", err) | ||
| default: | ||
| return nil | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/prometheus/client_golang/prometheus/collectors" | ||
| ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" | ||
| ) | ||
|
|
||
| // runtimeMetrics defines the controller-runtime, workqueue, REST client and | ||
| // leader election series an operator exposes, with the same names, label sets | ||
| // and bucket layouts as the real ones. The real vectors live in | ||
| // controller-runtime's internal packages; runtime_test.go guards this copy | ||
| // against drift. | ||
| type runtimeMetrics struct { | ||
| reconcileTotal *prometheus.CounterVec | ||
| reconcileErrors *prometheus.CounterVec | ||
| reconcilePanics *prometheus.CounterVec | ||
| reconcileTime *prometheus.HistogramVec | ||
| maxConcurrent *prometheus.GaugeVec | ||
| activeWorkers *prometheus.GaugeVec | ||
| queueDepth *prometheus.GaugeVec | ||
| queueAdds *prometheus.CounterVec | ||
| queueDuration *prometheus.HistogramVec | ||
| workDuration *prometheus.HistogramVec | ||
| queueUnfinished *prometheus.GaugeVec | ||
| queueLongestRunning *prometheus.GaugeVec | ||
| queueRetries *prometheus.CounterVec | ||
| restRequests *prometheus.CounterVec | ||
| leader *prometheus.GaugeVec | ||
| } | ||
|
|
||
| // Label names and the reconcile result value the lookalike series share with | ||
| // controller-runtime. | ||
| const ( | ||
| labelController = "controller" | ||
| labelName = "name" | ||
| resultError = "error" | ||
| ) | ||
|
|
||
| // reconcileTimeBuckets mirrors controller-runtime's ReconcileTime histogram. | ||
| var reconcileTimeBuckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, | ||
| 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 60} | ||
|
|
||
| func newRuntimeMetrics(reg prometheus.Registerer) *runtimeMetrics { | ||
| m := &runtimeMetrics{ | ||
| reconcileTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Name: "controller_runtime_reconcile_total", | ||
| Help: "Total number of reconciliations per controller", | ||
| }, []string{labelController, "result"}), | ||
| reconcileErrors: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Name: "controller_runtime_reconcile_errors_total", | ||
| Help: "Total number of reconciliation errors per controller", | ||
| }, []string{labelController}), | ||
| reconcilePanics: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Name: "controller_runtime_reconcile_panics_total", | ||
| Help: "Total number of reconciliation panics per controller", | ||
| }, []string{labelController}), | ||
| reconcileTime: prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
| Name: "controller_runtime_reconcile_time_seconds", | ||
| Help: "Length of time per reconciliation per controller", | ||
| Buckets: reconcileTimeBuckets, | ||
| }, []string{labelController}), | ||
| maxConcurrent: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Name: "controller_runtime_max_concurrent_reconciles", | ||
| Help: "Maximum number of concurrent reconciles per controller", | ||
| }, []string{labelController}), | ||
| activeWorkers: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Name: "controller_runtime_active_workers", | ||
| Help: "Number of currently used workers per controller", | ||
| }, []string{labelController}), | ||
| queueDepth: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.DepthKey, | ||
| Help: "Current depth of workqueue by workqueue and priority", | ||
| }, []string{labelName, labelController, "priority"}), | ||
| queueAdds: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.AddsKey, | ||
| Help: "Total number of adds handled by workqueue", | ||
| }, []string{labelName, labelController}), | ||
| queueDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.QueueLatencyKey, | ||
| Help: "How long in seconds an item stays in workqueue before being requested", | ||
| Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12), | ||
| }, []string{labelName, labelController}), | ||
| workDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.WorkDurationKey, | ||
| Help: "How long in seconds processing an item from workqueue takes.", | ||
| Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12), | ||
| }, []string{labelName, labelController}), | ||
| queueUnfinished: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.UnfinishedWorkKey, | ||
| Help: "How many seconds of work has been done that " + | ||
| "is in progress and hasn't been observed by work_duration. Large " + | ||
| "values indicate stuck threads. One can deduce the number of stuck " + | ||
| "threads by observing the rate at which this increases.", | ||
| }, []string{labelName, labelController}), | ||
| queueLongestRunning: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.LongestRunningProcessorKey, | ||
| Help: "How many seconds has the longest running " + | ||
| "processor for workqueue been running.", | ||
| }, []string{labelName, labelController}), | ||
| queueRetries: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Subsystem: ctrlmetrics.WorkQueueSubsystem, Name: ctrlmetrics.RetriesKey, | ||
| Help: "Total number of items added to the workqueue with a non-zero delay (rate-limited requeues, explicit RequeueAfter or AddAfter calls)", | ||
| }, []string{labelName, labelController}), | ||
| restRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
| Name: "rest_client_requests_total", | ||
| Help: "Number of HTTP requests, partitioned by status code, method, and host.", | ||
| }, []string{"code", "method", "host"}), | ||
| leader: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
| Name: "leader_election_master_status", | ||
| Help: "Gauge of if the reporting system is master of the relevant lease, 0 indicates backup, 1 indicates master. 'name' is the string used to identify the lease. Please make sure to group by name.", | ||
| }, []string{labelName}), | ||
| } | ||
| reg.MustRegister( | ||
| m.reconcileTotal, m.reconcileErrors, m.reconcilePanics, m.reconcileTime, m.maxConcurrent, m.activeWorkers, | ||
| m.queueDepth, m.queueAdds, m.queueDuration, m.workDuration, m.queueUnfinished, m.queueLongestRunning, m.queueRetries, | ||
| m.restRequests, m.leader, | ||
| collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), | ||
| collectors.NewGoCollector(), | ||
| ) | ||
| return m | ||
| } | ||
|
|
||
| // initController creates the zero-valued series controller-runtime creates | ||
| // when a controller starts, so panels show a flat line instead of no data. | ||
| func (m *runtimeMetrics) initController(name string, maxConcurrent int) { | ||
| for _, result := range []string{resultError, "requeue_after", "requeue", "success"} { | ||
| m.reconcileTotal.WithLabelValues(name, result).Add(0) | ||
| } | ||
| m.reconcileErrors.WithLabelValues(name).Add(0) | ||
| m.reconcilePanics.WithLabelValues(name).Add(0) | ||
| m.reconcileTime.WithLabelValues(name) | ||
| m.maxConcurrent.WithLabelValues(name).Set(float64(maxConcurrent)) | ||
| m.activeWorkers.WithLabelValues(name).Set(0) | ||
| m.queueDepth.WithLabelValues(name, name, "0").Set(0) | ||
| m.queueAdds.WithLabelValues(name, name).Add(0) | ||
| m.queueDuration.WithLabelValues(name, name) | ||
| m.workDuration.WithLabelValues(name, name) | ||
| m.queueUnfinished.WithLabelValues(name, name).Set(0) | ||
| m.queueLongestRunning.WithLabelValues(name, name).Set(0) | ||
| m.queueRetries.WithLabelValues(name, name).Add(0) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.