Spring Boot
Prometheus
Metrics
URI
Debugging

Unexplainable root uri in spring boot prometheus metrics

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When using Spring Boot Actuator with Micrometer and Prometheus, you may see HTTP request metrics with a uri="root" or uri="/" tag that you did not expect. This typically comes from health check probes (Kubernetes liveness/readiness), load balancer health checks, or bots/scanners hitting the root path. Spring Boot's Micrometer integration tags every HTTP request with a uri label, and requests to unmapped paths get tagged with uri="/**" or the actual path. Understanding where these requests originate and how to filter or relabel them is key to keeping your Prometheus metrics clean.

The Symptom

 
# Prometheus metrics showing unexpected root URI
http_server_requests_seconds_count{method="GET",outcome="SUCCESS",status="200",uri="/",} 15234.0
http_server_requests_seconds_count{method="GET",outcome="CLIENT_ERROR",status="404",uri="/**",} 89.0

The uri="/" metric has a high count but you have no controller mapped to /.

Common Sources of Root URI Requests

  1. Kubernetes probes: Default liveness/readiness probes hit / if not configured to a specific path
  2. Load balancer health checks: AWS ALB, GCP LB, or Nginx default health checks hit /
  3. Bots and scanners: Web crawlers and vulnerability scanners probe the root path
  4. Spring Boot welcome page: If you have an index.html in static/, Spring Boot serves it at /
  5. Actuator redirects: Misconfigured actuator base path can cause redirects through /

Identifying the Source

java
1// Add a filter to log requests to the root path
2@Component
3public class RootRequestLogger implements Filter {
4
5    private static final Logger log = LoggerFactory.getLogger(RootRequestLogger.class);
6
7    @Override
8    public void doFilter(ServletRequest request, ServletResponse response,
9                         FilterChain chain) throws IOException, ServletException {
10        HttpServletRequest httpRequest = (HttpServletRequest) request;
11        if ("/".equals(httpRequest.getRequestURI())) {
12            log.info("Root request from: {} User-Agent: {}",
13                httpRequest.getRemoteAddr(),
14                httpRequest.getHeader("User-Agent"));
15        }
16        chain.doFilter(request, response);
17    }
18}

Fix 1: Configure Kubernetes Probes Properly

yaml
1# deployment.yaml — point probes to actuator health endpoint
2spec:
3  containers:
4    - name: app
5      livenessProbe:
6        httpGet:
7          path: /actuator/health/liveness
8          port: 8080
9        initialDelaySeconds: 30
10        periodSeconds: 10
11      readinessProbe:
12        httpGet:
13          path: /actuator/health/readiness
14          port: 8080
15        initialDelaySeconds: 10
16        periodSeconds: 5
yaml
1# application.yml — enable probe endpoints
2management:
3  endpoint:
4    health:
5      probes:
6        enabled: true
7  health:
8    livenessState:
9      enabled: true
10    readinessState:
11      enabled: true

Fix 2: Filter Out Root URI from Metrics

Use a MeterFilter to ignore or rename the root URI in metrics.

java
1import io.micrometer.core.instrument.MeterRegistry;
2import io.micrometer.core.instrument.config.MeterFilter;
3import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6
7@Configuration
8public class MetricsConfig {
9
10    @Bean
11    public MeterRegistryCustomizer<MeterRegistry> metricsFilter() {
12        return registry -> registry.config()
13            .meterFilter(MeterFilter.deny(id ->
14                id.getName().equals("http.server.requests") &&
15                "/".equals(id.getTag("uri"))
16            ));
17    }
18}

Or rename the URI tag to group health check traffic:

java
1@Bean
2public MeterRegistryCustomizer<MeterRegistry> renameRootUri() {
3    return registry -> registry.config()
4        .meterFilter(MeterFilter.replaceTagValues("uri",
5            actualUri -> "/".equals(actualUri) ? "/health-check" : actualUri
6        ));
7}

Fix 3: Customize URI Tag via WebMvcTagsContributor

java
1import io.micrometer.core.instrument.Tag;
2import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
3import org.springframework.stereotype.Component;
4
5@Component
6public class CustomUriTagContributor implements WebMvcTagsContributor {
7
8    @Override
9    public Iterable<Tag> getTags(HttpServletRequest request,
10                                  HttpServletResponse response,
11                                  Object handler, Throwable exception) {
12        // Return empty — let default tags handle it
13        return Collections.emptyList();
14    }
15
16    @Override
17    public Iterable<Tag> getLongRequestTags(HttpServletRequest request, Object handler) {
18        return Collections.emptyList();
19    }
20}

Fix 4: Prometheus Relabeling

Filter out the root URI at the Prometheus scrape level:

yaml
1# prometheus.yml
2scrape_configs:
3  - job_name: 'spring-boot-app'
4    metrics_path: /actuator/prometheus
5    static_configs:
6      - targets: ['app:8080']
7    metric_relabel_configs:
8      - source_labels: [uri]
9        regex: '/'
10        action: drop

Common Pitfalls

  • Kubernetes default probes hitting root path: If you do not configure livenessProbe.httpGet.path, some Kubernetes setups default to /. Every probe interval generates a metric entry, inflating uri="/" counts. Always set explicit probe paths pointing to /actuator/health.
  • High cardinality from unmapped URIs: Requests to paths like /wp-admin, /phpmyadmin, or other scanner targets create unique uri tag values, causing cardinality explosion in Prometheus. Spring Boot 2.6+ replaces unmapped paths with uri="/**", but custom error handling may override this.
  • Filtering metrics too aggressively: Denying all uri="/" metrics also hides legitimate traffic if you have a controller mapped to /. Use the logging filter first to identify the source before filtering metrics.
  • Not setting management.server.port: Running actuator endpoints on the same port as the application means health check traffic shows up in application request metrics. Set management.server.port: 9090 to separate actuator traffic onto a different port, keeping application metrics clean.
  • Prometheus relabeling dropping all root metrics: The metric_relabel_configs drop rule applies to all metrics with uri="/", including any you intentionally want to monitor. Use more specific rules that also match status or method to target only health check traffic.

Summary

  • uri="/" in Prometheus metrics usually comes from Kubernetes probes, load balancer health checks, or scanners
  • Configure probes to hit /actuator/health/liveness and /actuator/health/readiness instead of /
  • Use MeterFilter.deny() to filter unwanted root URI metrics at the application level
  • Use Prometheus metric_relabel_configs to drop or rename metrics at the scrape level
  • Separate actuator and application traffic with management.server.port to keep metrics clean

Course illustration
Course illustration

All Rights Reserved.