Prometheus: name-only tags are exported as empty labels and cannot be filtered reliably
I have found these related issues/pull requests
Related issues: #5634 requests exposing Uptime Kuma tags as Prometheus labels; #6470 concerns dynamic tag handling in server/prometheus.js. This report is different and more specific: tags that exist but have no value are exported with an empty label value, making them indistinguishable from absent labels in PromQL.
️ Security Policy
- I have read and agree to Uptime Kuma's Security Policy.
Description
Uptime Kuma allows creating a tag with only a name and no value, for example sslcert or httpcheck. The Prometheus exporter currently emits these as sslcert="" and httpcheck="".
In PromQL, matching an empty label value also matches series where that label is absent, so a selector such as monitor_uptime_ratio{sslcert=""} cannot reliably mean "only monitors that have the sslcert tag". This also cannot be fixed cleanly with metric_relabel_configs, because an absent source label and an empty source label are indistinguishable at that stage.
The behavior comes from mapTagsToLabels() in server/prometheus.js: for a name-only tag, the sanitized value is empty and nothing is pushed into the tag array, which is ultimately exported as an empty label.
Tested code workaround
I tested a minimal change that uses the sanitized tag name as the value only when the tag value is empty, while preserving existing values:
let tagValue = Prometheus.sanitizeForPrometheus(tag.value || "");
if (tagValue === "") {
tagValue = sanitizedTag;
}
mappedTags[sanitizedTag].push(tagValue);
mappedTags[sanitizedTag] = mappedTags[sanitizedTag].sort();
With this change, a name-only sslcert tag is exported as sslcert="sslcert", while tags with explicit values continue to behave as before.
Tested Docker workaround
I applied the change reproducibly during a custom image build rather than bind-mounting a complete modified prometheus.js:
FROM louislam/uptime-kuma:2 AS uptime-kuma-base
FROM alpine:3.22 AS patch-builder
RUN apk add --no-cache patch
WORKDIR /app
COPY --from=uptime-kuma-base /app/server/prometheus.js server/prometheus.js
COPY prometheus.patch /tmp/prometheus.patch
RUN patch --batch --forward -p1 < /tmp/prometheus.patch
FROM uptime-kuma-base
COPY --from=patch-builder /app/server/prometheus.js /app/server/prometheus.js
This keeps the official runtime image unchanged except for the patched file, and the patch utility exists only in the temporary build stage. On each rebuild, the patch is applied to the current upstream prometheus.js; if upstream changes make it incompatible, the image build fails instead of silently using an outdated full-file override.
This Docker setup is only a tested workaround. The requested upstream fix is the small change in mapTagsToLabels() so custom images are no longer necessary.
Reproduction steps
- Create a monitor in Uptime Kuma.
- Add a tag with a name such as
sslcert, leaving its value empty. - Save the monitor.
- Query the
/metricsendpoint. - Locate a metric for that monitor, for example
monitor_uptime_ratio. - Observe that the tag is exported as
sslcert="". - In Prometheus/Grafana, query
monitor_uptime_ratio{sslcert=""}and observe that series without the label can also match.
Expected behavior
A name-only tag should remain distinguishable from an absent tag after Prometheus ingests the metric. For example, sslcert could be exported as sslcert="sslcert" (or another non-empty representation chosen by the maintainers), so PromQL can reliably filter monitors carrying that tag.
Actual Behavior
The exporter emits sslcert="". Prometheus treats a matcher for an empty label value as also matching series where the label is absent, so the tag cannot be used as a reliable presence filter.
Uptime-Kuma Version
2.x (Docker image louislam/uptime-kuma:2; exact patch version not available)
Operating System and Arch
Linux (Docker host; exact distribution and architecture not available)
Browser
Not browser-specific; reproduced through the Prometheus /metrics endpoint
️ Deployment Environment
- Runtime Environment: Docker
- Database: SQLite (embedded)
- Database Storage: bind-mounted
/app/data - Uptime Kuma image:
louislam/uptime-kuma:2 - Prometheus integration:
/metricsendpoint scraped by Prometheus and queried in Grafana
The workaround was also tested in a custom Docker image by applying a small patch during the image build. This avoids bind-mounting a complete modified prometheus.js: each new image contributes the current upstream file, and the build fails if the patch is no longer compatible.
Relevant log output
Source: louislam/uptime-kuma