Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 027 / 100

Monitoring Spring Boot and Go: best practices explained

Metrics in other stacks.

Expona Admin5 min read

Monitoring Spring Boot and Go: best practices explained

When you're running production services across different technology stacks, keeping metrics consistent becomes a survival skill. Whether your team is using Spring Boot for a financial backend or Go for a high-throughput API gateway, the ability to collect, export, and query metrics with the same tooling and naming conventions saves countless debugging hours and prevents metric sprawl. This guide walks through how to instrument both stacks effectively and maintain consistency across your observability platform.

Why metrics matter across multiple stacks

Most teams don't standardize on a single language. You'll have Spring Boot services running alongside Go microservices, perhaps Node.js event processors, and Python data pipelines. If each team instruments their services differently—or uses incompatible metric formats—you end up with fragmented observability. Request latencies measured in milliseconds in one service and nanoseconds in another. Custom metrics that only one team understands. Alert rules that don't translate across the platform.

Prometheus solves this by defining a standard metric format and encouraging a consistent naming schema. Spring Boot and Go both have mature instrumentation libraries that emit Prometheus-compatible metrics. The trick is using them consistently so your dashboards and alerts actually make sense.

Spring Boot: Micrometer Prometheus registry

Spring Boot doesn't natively speak Prometheus. That's where Micrometer comes in. It's an abstraction layer for application metrics that supports multiple backends—Prometheus, Datadog, CloudWatch, and others. When you wire up Micrometer with the Prometheus registry, your Spring application automatically collects and exposes metrics in Prometheus format.

Start by adding the dependency to your pom.xml:

yaml
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.12.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Configure it in your application.yml:

yaml
management:
  endpoints:
    web:
      exposure:
        include: prometheus
  metrics:
    tags:
      application: order-service
      environment: production
    enable:
      jvm: true
      process: true

Now your metrics are exposed at /actuator/prometheus. Micrometer automatically collects JVM memory, garbage collection, thread counts, and HTTP request metrics. You get runtime observability for free with sensible defaults.

For custom business metrics, inject the MeterRegistry:

yaml
@Service
public class OrderService {
    private final MeterRegistry meterRegistry;
    private final Counter ordersCreated;

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.ordersCreated = Counter.builder("orders.created")
            .description("Total orders created")
            .tag("service", "order-service")
            .register(meterRegistry);
    }

    public void createOrder(Order order) {
        // business logic
        ordersCreated.increment();
    }
}

The metric name follows the convention: orders.created. Micrometer converts dots to underscores when exporting to Prometheus, so it becomes orders_created_total. Always use lowercase, dot-separated names in your code. Let the framework handle the transport format.

Go: prometheus/client_golang

The Go ecosystem's approach is more manual and direct. The prometheus/client_golang library gives you fine-grained control over what you measure. There's no magic framework doing it for you—you explicitly register collectors and record observations.

Install the library:

yaml
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp

Define your metrics at the package level:

yaml
package orders

import "github.com/prometheus/client_golang/prometheus"

var (
    ordersCreated = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "orders_created_total",
            Help: "Total orders created",
        },
        []string{"service"},
    )
    
    orderProcessingDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "order_processing_duration_seconds",
            Help:    "Order processing duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"service", "status"},
    )
)

func init() {
    prometheus.MustRegister(ordersCreated)
    prometheus.MustRegister(orderProcessingDuration)
}

Use the metrics in your handlers:

yaml
func CreateOrder(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    defer func() {
        duration := time.Since(start).Seconds()
        orderProcessingDuration.WithLabelValues("order-service", "success").Observe(duration)
    }()

    // business logic
    ordersCreated.WithLabelValues("order-service").Inc()
    w.WriteHeader(http.StatusCreated)
}

Expose metrics with a handler:

yaml
func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/orders", CreateOrder)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Notice the naming: orders_created_total for counters, order_processing_duration_seconds for histograms. Go follows Prometheus conventions strictly. The metric names are explicit in your code, not derived by a framework.

Runtime metrics for free

Both stacks hand you runtime metrics without any additional work. Spring Boot's Actuator collects JVM metrics automatically: heap memory, garbage collection pause times, thread counts, class loading. Go's prometheus/client_golang provides a prometheus.NewGoCollector() that tracks goroutines, memory, and garbage collection.

In Go, register it explicitly:

yaml
func init() {
    prometheus.MustRegister(prometheus.NewGoCollector())
    prometheus.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
}

You should enable these in every service. Runtime metrics catch memory leaks, thread pool exhaustion, and garbage collection pressure before they cascade into production incidents. They're also uniform across all your services—same metric names, same semantics. When you see process_resident_memory_bytes spike across your dashboard, you know exactly what you're looking at.

Same naming conventions everywhere

Consistency in metric naming prevents your observability from becoming a liability. Adopt these conventions across all stacks:

  • Counters: namespace_operation_total (e.g., api_requests_total)
  • Gauges: namespace_metric_unit (e.g., database_connections_active)
  • Histograms: namespace_operation_duration_seconds (e.g., http_request_duration_seconds)
  • Summaries: namespace_operation_duration_seconds (same as histograms)
  • Labels: Always include service, environment, and a primary dimension (e.g., status, method)

Apply these rules in both Spring Boot and Go. In Spring Boot, use dot notation in your code and let Micrometer convert it. In Go, use underscores in the metric name itself. The endpoint output will look identical in Prometheus format.

When a team adds a new metric, they should check existing metric names first. Don't create order_process_time_ms and order_processing_seconds for the same thing in different services. Normalize the unit and the name format. Your future self will thank you when you're writing alert rules that apply across the platform.

Common mistakes

  • Mixing units: One service reports latency in milliseconds, another in nanoseconds. Always use seconds for time-based metrics. Prometheus convention is clear on this.
  • Cardinality explosion: Don't use user ID, order ID, or any high-cardinality value as a label. This explodes your metrics storage and crashes Prometheus. Aggregate at the metric level instead.
  • Forgetting the _total suffix: Counters in Prometheus should end with _total. It makes it obvious they're monotonically increasing. Both Micrometer and client_golang append this automatically if you follow conventions.
  • Collecting too frequently: Micrometer defaults to collecting every minute. In Go, you control collection timing. Don't scrape metrics more than every 15 seconds unless you have a specific reason. Excessive scraping kills your database and metrics backend.
  • Missing service tags: Always include a service or app label so you know which service the metric came from. This is non-negotiable when running dozens of microservices.

Key takeaways

  • Spring Boot uses Micrometer with the Prometheus registry to expose metrics at /actuator/prometheus. Go uses prometheus/client_golang and you manually register collectors and expose them on an HTTP endpoint.
  • Runtime metrics—JVM stats, garbage collection, memory, goroutines—come for free in both stacks. Enable them and use them. They catch real problems.
  • Adopt consistent naming conventions across all services: lowercase, dot-separated (Spring) or underscore-separated (Go), include units, use _total for counters, _seconds for time.
  • Always include service and environment labels. Never use high-cardinality values like user IDs or request IDs as labels.
  • Micrometer abstracts the transport layer; client_golang puts you closer to the metal. Both produce the same Prometheus output format. Learn your tool, follow the conventions, and your observability platform becomes an asset instead of a burden.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles