Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .mise/mise.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 28 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ onto it. Notable values (see [`charts/kromgo/values.yaml`](charts/kromgo/values.
| `secret.prometheusUrl` / `.existingSecret` | inject `PROMETHEUS_URL` from a Secret when the URL carries credentials |
| `ingress.enabled` | expose the app via an Ingress |
| `httpRoute.enabled` | expose the app via a Gateway API `HTTPRoute` (set `parentRefs` + `hostnames`) |
| `monitoring.serviceMonitor.enabled` | scrape `/metrics` on the health port (Prometheus Operator) |
| `monitoring.serviceMonitor.enabled` | scrape `/metrics` on the metrics port (Prometheus Operator) |

Every value is documented in the chart's generated README,
[`charts/kromgo/README.md`](charts/kromgo/README.md), built from
Expand All @@ -101,19 +101,20 @@ point your editor's YAML language server at it for inline completion and validat

### Environment variables

| Variable | Required | Default | Description |
| ---------------------- | -------- | --------- | ------------------------------------------- |
| `PROMETHEUS_URL` | yes | — | URL of your Prometheus instance |
| `SERVER_HOST` | no | `0.0.0.0` | Host to bind the main server |
| `SERVER_PORT` | no | `8080` | Port for the main server |
| `HEALTH_HOST` | no | `0.0.0.0` | Host to bind the health server |
| `HEALTH_PORT` | no | `8081` | Port for the health/metrics server |
| `SERVER_LOGGING` | no | `false` | Enable HTTP request access logging |
| `SERVER_READ_TIMEOUT` | no | — | HTTP read timeout (e.g. `5s`) |
| `SERVER_WRITE_TIMEOUT` | no | — | HTTP write timeout (e.g. `10s`) |
| `QUERY_TIMEOUT` | no | `30s` | Timeout applied to each Prometheus query |
| `LOG_LEVEL` | no | `info` | Log level: `debug`, `info`, `warn`, `error` |
| `LOG_FORMAT` | no | `json` | Log format: `json` or `text` |
| Variable | Required | Default | Description |
| ---------------------- | -------- | ------- | -------------------------------------------- |
| `PROMETHEUS_URL` | yes | — | URL of your Prometheus instance |
| `SERVER_HOST` | no | _(all)_ | Bind host; empty = all families (dual-stack) |
| `SERVER_PORT` | no | `8080` | Port for the main server |
| `METRICS_HOST` | no | _(all)_ | Bind host for the metrics listener |
| `METRICS_ENABLED` | no | `true` | Serve Prometheus /metrics; off ⇒ no listener |
| `METRICS_PORT` | no | `8081` | Metrics listen port (/metrics only) |
| `SERVER_LOGGING` | no | `false` | Enable HTTP request access logging |
| `SERVER_READ_TIMEOUT` | no | — | HTTP read timeout (e.g. `5s`) |
| `SERVER_WRITE_TIMEOUT` | no | — | HTTP write timeout (e.g. `10s`) |
| `QUERY_TIMEOUT` | no | `30s` | Timeout applied to each Prometheus query |
| `LOG_LEVEL` | no | `info` | Log level: `debug`, `info`, `warn`, `error` |
| `LOG_FORMAT` | no | `json` | Log format: `json` or `text` |

### Defaults

Expand Down Expand Up @@ -289,14 +290,14 @@ Besides CEL's built-ins (arithmetic, comparisons, ternary `?:`, `in`) the enviro
On top of those, these formatting helpers are available (hand-rolled — kromgo has no external
humanize dependency, so the output is exactly as below):

| Function | Example | Result | Notes |
| ------------------------------ | --------------------------------- | --------- | ------------------------------------------- |
| Function | Example | Result | Notes |
| ------------------------------ | --------------------------------- | --------- | ---------------------------------------------------------- |
| `humanize(result)` | `humanize(93166031.0)` | `93.17M` | SI metric prefixes (powers of 1000), 4 sig figs, unit-less |
| `humanizeBytes(result)` | `humanizeBytes(1500000.0)` | `1.5MB` | SI decimal units (powers of 1000), no space |
| `humanizeCommas(result)` | `humanizeCommas(157121.0)` | `157,121` | comma thousands grouping |
| `humanizeFloat(result)` | `humanizeFloat(2.50)` | `2.5` | plain decimal, trailing zeros stripped |
| `humanizeDuration(result)` | `humanizeDuration(9000.0)` | `2h30m` | **seconds** → compact time span |
| `humanizeDurationDays(result)` | `humanizeDurationDays(5961600.0)` | `69d` | **seconds** → whole days, no roll-up |
| `humanizeBytes(result)` | `humanizeBytes(1500000.0)` | `1.5MB` | SI decimal units (powers of 1000), no space |
| `humanizeCommas(result)` | `humanizeCommas(157121.0)` | `157,121` | comma thousands grouping |
| `humanizeFloat(result)` | `humanizeFloat(2.50)` | `2.5` | plain decimal, trailing zeros stripped |
| `humanizeDuration(result)` | `humanizeDuration(9000.0)` | `2h30m` | **seconds** → compact time span |
| `humanizeDurationDays(result)` | `humanizeDurationDays(5961600.0)` | `69d` | **seconds** → whole days, no roll-up |

`humanizeDuration` takes **seconds** (so it drops onto a `time() - created_ts` query directly) and
adapts to the magnitude, emitting the up-to-three most-significant units — `90` → `1m30s`, `9000` →
Expand Down Expand Up @@ -522,10 +523,10 @@ When nothing is visible the gallery shows a short hint instead.

## Ports

| Port | Purpose |
| ------ | -------------------------------------------------------------- |
| `8080` | Main server — badge and graph endpoints |
| `8081` | Health server — `/healthz`, `/readyz`, `/metrics` (Prometheus) |
| Port | Purpose |
| ------ | --------------------------------------------------------------------- |
| `8080` | Main server — badge/graph endpoints + `/healthz`, `/readyz` probes |
| `8081` | Metrics server — `/metrics` (Prometheus); optional, off ⇒ no listener |

The health server's `/metrics` endpoint exposes Go runtime metrics plus
`kromgo_requests_total{kind, id, format}` — a counter of requests handled, broken down by endpoint
Expand Down Expand Up @@ -678,8 +679,8 @@ kromgo is built to face the public web. Its posture:

Operational guidance:

- **Expose only the main port (`8080`).** The health port (`8081`) serves `/metrics` and probes —
keep it on the internal network.
- **Expose only the main port (`8080`).** The metrics port (`8081`) serves `/metrics` —
keep it on the internal network. Health probes ride the main port.
- **Terminate TLS and rate limit at your reverse proxy** (see [Rate limiting](#rate-limiting)).
- Treat the config as trusted (it's operator-controlled). Fonts are compiled-in (never read from
disk), and CEL expressions run sandboxed (no env/file/network access).
Expand Down
9 changes: 5 additions & 4 deletions charts/kromgo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Kubernetes: `>=1.25.0-0`
| ingress.enabled | bool | `false` | Expose the UI via an Ingress. |
| ingress.hosts | list | `[{"host":"kromgo.example.com","paths":[{"path":"/","pathType":"Prefix"}]}]` | Ingress hosts and their paths. |
| ingress.tls | list | `[]` | Ingress TLS configuration. |
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"health"},"initialDelaySeconds":10,"periodSeconds":20}` | Liveness probe. |
| livenessProbe | object | `{"httpGet":{"path":"/healthz","port":"http"},"initialDelaySeconds":10,"periodSeconds":20}` | Liveness probe. Targets /healthz on the main http port, so it works regardless of the metrics toggle. |
| monitoring.serviceMonitor.annotations | object | `{}` | ServiceMonitor annotations. |
| monitoring.serviceMonitor.enabled | bool | `false` | Create a Prometheus Operator ServiceMonitor (requires its CRDs). |
| monitoring.serviceMonitor.interval | string | `"30s"` | Scrape interval. |
Expand All @@ -86,7 +86,7 @@ Kubernetes: `>=1.25.0-0`
| podAnnotations | object | `{}` | Annotations added to the pod. |
| podLabels | object | `{}` | Labels added to the pod. |
| podSecurityContext | object | `{"fsGroup":65532,"runAsGroup":65532,"runAsNonRoot":true,"runAsUser":65532,"seccompProfile":{"type":"RuntimeDefault"}}` | Pod-level securityContext (runs as non-root uid/gid 65532 with the RuntimeDefault seccomp profile). |
| readinessProbe | object | `{"httpGet":{"path":"/readyz","port":"health"},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe. |
| readinessProbe | object | `{"httpGet":{"path":"/readyz","port":"http"},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe. Targets /readyz on the main http port. |
| replicaCount | int | `1` | Number of kromgo replicas (it queries Prometheus per request and is stateless behind the Service). |
| resources | object | `{}` | Pod resource requests/limits. |
| secret.existingSecret | string | `""` | Existing Secret with a PROMETHEUS_URL key; takes precedence over the inline value below. |
Expand All @@ -97,8 +97,9 @@ Kubernetes: `>=1.25.0-0`
| server.queryTimeout | string | `"30s"` | QUERY_TIMEOUT — bounds each outbound Prometheus query (Go duration). |
| server.readTimeout | string | `"15s"` | SERVER_READ_TIMEOUT (Go duration); "0" disables the read deadline. |
| server.writeTimeout | string | `"60s"` | SERVER_WRITE_TIMEOUT (Go duration, must exceed queryTimeout); "0" disables it. |
| service.metricsPort | int | `8081` | Health + /metrics port. |
| service.port | int | `8080` | Badge / graph / gallery port. |
| service.metricsEnabled | bool | `true` | Expose Prometheus metrics at /metrics on metricsPort (METRICS_ENABLED). Disabling removes the metrics listener, container port, and Service port entirely; health probes are unaffected (they target the http port). |
| service.metricsPort | int | `8081` | Metrics listen port (/metrics only), kept off the public port. |
| service.port | int | `8080` | Badge / graph / gallery port; also serves the /healthz and /readyz probes. |
| service.type | string | `"ClusterIP"` | Service type. |
| serviceAccount.annotations | object | `{}` | Annotations for the ServiceAccount. |
| serviceAccount.automount | bool | `false` | Automount the ServiceAccount API token (off by default: kromgo talks to Prometheus, not the cluster API). |
Expand Down
8 changes: 7 additions & 1 deletion charts/kromgo/templates/deployment.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ spec:
- name: SERVER_LOGGING
value: "true"
{{- end }}
{{- if not .Values.service.metricsEnabled }}
- name: METRICS_ENABLED
value: "false"
{{- end }}
{{- with .Values.server.extraEnv }}
{{- tpl (toYaml .) $ | nindent 12 }}
{{- end }}
Expand All @@ -76,9 +80,11 @@ spec:
- name: http
containerPort: 8080
protocol: TCP
- name: health
{{- if .Values.service.metricsEnabled }}
- name: metrics
containerPort: 8081
protocol: TCP
{{- end }}
livenessProbe:
{{- tpl (toYaml .Values.livenessProbe) $ | nindent 12 }}
readinessProbe:
Expand Down
4 changes: 3 additions & 1 deletion charts/kromgo/templates/service.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ spec:
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
{{- if .Values.service.metricsEnabled }}
- name: metrics
port: {{ .Values.service.metricsPort }}
targetPort: health
targetPort: metrics
protocol: TCP
{{- end }}
selector:
{{- include "kromgo.selectorLabels" . | nindent 4 }}
4 changes: 2 additions & 2 deletions charts/kromgo/templates/tests/test-connection.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ spec:
capabilities:
drop:
- ALL
# /readyz on the health/metrics port returns a static 200 (no Prometheus
# /readyz on the main http port returns a static 200 (no Prometheus
# dependency), so this checks purely that the Service routes to a running,
# listening pod. curl -f fails on a non-2xx (or a refused connection), failing
# `helm test`; -sS stays quiet but still surfaces errors, and the body goes to
Expand All @@ -39,4 +39,4 @@ spec:
- curl
args:
- -fsS
- http://{{ include "kromgo.fullname" . }}:{{ .Values.service.metricsPort }}/readyz
- http://{{ include "kromgo.fullname" . }}:{{ .Values.service.port }}/readyz
25 changes: 25 additions & 0 deletions charts/kromgo/tests/deployment_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,28 @@ tests:
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false

# Pins the port standard: probes target the http port, and disabling metrics
# removes the metrics containerPort without touching them.
- it: keeps probes working when metrics are disabled
template: deployment.tpl
set:
service.metricsEnabled: false
asserts:
- notContains:
path: spec.template.spec.containers[0].ports
content:
name: metrics
containerPort: 8081
protocol: TCP
- contains:
path: spec.template.spec.containers[0].env
content:
name: METRICS_ENABLED
value: "false"
- equal:
path: spec.template.spec.containers[0].livenessProbe.httpGet.port
value: http
- equal:
path: spec.template.spec.containers[0].readinessProbe.httpGet.port
value: http
2 changes: 1 addition & 1 deletion charts/kromgo/tests/test-connection_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ tests:
value: true
- contains:
path: spec.containers[0].args
content: http://RELEASE-NAME-kromgo:8081/readyz
content: http://RELEASE-NAME-kromgo:8080/readyz
18 changes: 12 additions & 6 deletions charts/kromgo/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"type": "object"
},
"livenessProbe": {
"description": "Liveness probe.",
"description": "Liveness probe. Targets /healthz on the main http port, so it works regardless of the metrics toggle.",
"properties": {
"httpGet": {
"properties": {
Expand All @@ -339,7 +339,7 @@
"type": "string"
},
"port": {
"default": "health",
"default": "http",
"title": "port",
"type": "string"
}
Expand Down Expand Up @@ -494,7 +494,7 @@
"type": "object"
},
"readinessProbe": {
"description": "Readiness probe.",
"description": "Readiness probe. Targets /readyz on the main http port.",
"properties": {
"httpGet": {
"properties": {
Expand All @@ -504,7 +504,7 @@
"type": "string"
},
"port": {
"default": "health",
"default": "http",
"title": "port",
"type": "string"
}
Expand Down Expand Up @@ -639,15 +639,21 @@
},
"service": {
"properties": {
"metricsEnabled": {
"default": true,
"description": "Expose Prometheus metrics at /metrics on metricsPort (METRICS_ENABLED). Disabling removes the metrics listener, container port, and Service port entirely; health probes are unaffected (they target the http port).",
"title": "metricsEnabled",
"type": "boolean"
},
"metricsPort": {
"default": 8081,
"description": "Health + /metrics port.",
"description": "Metrics listen port (/metrics only), kept off the public port.",
"title": "metricsPort",
"type": "integer"
},
"port": {
"default": 8080,
"description": "Badge / graph / gallery port.",
"description": "Badge / graph / gallery port; also serves the /healthz and /readyz probes.",
"title": "port",
"type": "integer"
},
Expand Down
14 changes: 8 additions & 6 deletions charts/kromgo/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,11 @@ securityContext:
service:
# -- Service type.
type: ClusterIP
# -- Badge / graph / gallery port.
# -- Badge / graph / gallery port; also serves the /healthz and /readyz probes.
port: 8080
# -- Health + /metrics port.
# -- Expose Prometheus metrics at /metrics on metricsPort (METRICS_ENABLED). Disabling removes the metrics listener, container port, and Service port entirely; health probes are unaffected (they target the http port).
metricsEnabled: true
# -- Metrics listen port (/metrics only), kept off the public port.
metricsPort: 8081

ingress:
Expand Down Expand Up @@ -251,18 +253,18 @@ resources: {}
# cpu: 10m
# memory: 64Mi

# -- Liveness probe.
# -- Liveness probe. Targets /healthz on the main http port, so it works regardless of the metrics toggle.
livenessProbe:
httpGet:
path: /healthz
port: health
port: http
initialDelaySeconds: 10
periodSeconds: 20
# -- Readiness probe.
# -- Readiness probe. Targets /readyz on the main http port.
readinessProbe:
httpGet:
path: /readyz
port: health
port: http
initialDelaySeconds: 5
periodSeconds: 10

Expand Down
8 changes: 5 additions & 3 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,17 @@ func TestLoad_Style(t *testing.T) {
func TestLoadServer_Defaults(t *testing.T) {
// Clear any inherited env so envDefault applies. t.Setenv registers the
// restore; os.Unsetenv then removes the var for the duration of the test.
for _, k := range []string{"SERVER_PORT", "HEALTH_PORT", "QUERY_TIMEOUT", "SERVER_LOGGING", "SERVER_READ_TIMEOUT", "SERVER_WRITE_TIMEOUT"} {
for _, k := range []string{"SERVER_PORT", "METRICS_ENABLED", "METRICS_PORT", "QUERY_TIMEOUT", "SERVER_LOGGING", "SERVER_READ_TIMEOUT", "SERVER_WRITE_TIMEOUT"} {
t.Setenv(k, "")
_ = os.Unsetenv(k)
}
sc, err := LoadServer()
require.NoError(t, err)
assert.Equal(t, "0.0.0.0", sc.ServerHost)
// Empty host = the unspecified address (dual-stack), not IPv4-only 0.0.0.0.
assert.Empty(t, sc.ServerHost)
assert.Equal(t, 8080, sc.ServerPort)
assert.Equal(t, 8081, sc.HealthPort)
assert.True(t, sc.MetricsEnabled)
assert.Equal(t, 8081, sc.MetricsPort)
assert.Equal(t, 30*time.Second, sc.QueryTimeout)
assert.Equal(t, 15*time.Second, sc.ServerReadTimeout, "read timeout defaults to a bounded value, not 0")
assert.Equal(t, 60*time.Second, sc.ServerWriteTimeout, "write timeout defaults above QueryTimeout")
Expand Down
13 changes: 10 additions & 3 deletions internal/config/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import (

// ServerConfig holds runtime server settings sourced from environment variables.
type ServerConfig struct {
ServerHost string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
// Hosts default to empty (the unspecified address), binding IPv4-only,
// IPv6-only, and dual-stack clusters alike; an explicit 0.0.0.0 would be
// IPv4-only. The main port also serves /healthz and /readyz.
ServerHost string `env:"SERVER_HOST" envDefault:""`
ServerPort int `env:"SERVER_PORT" envDefault:"8080"`

HealthHost string `env:"HEALTH_HOST" envDefault:"0.0.0.0"`
HealthPort int `env:"HEALTH_PORT" envDefault:"8081"`
// MetricsEnabled exposes Prometheus metrics at /metrics on MetricsPort.
// Disabling it removes the metrics listener entirely; the health probe
// endpoints live on the main port and are unaffected.
MetricsEnabled bool `env:"METRICS_ENABLED" envDefault:"true"`
MetricsHost string `env:"METRICS_HOST" envDefault:""`
MetricsPort int `env:"METRICS_PORT" envDefault:"8081"`

// ServerReadTimeout / ServerWriteTimeout bound reading a request and writing its
// response on the public listener; the defaults harden against slow-client
Expand Down
Loading
Loading