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:
<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:
management:
endpoints:
web:
exposure:
include: prometheus
metrics:
tags:
application: order-service
environment: production
enable:
jvm: true
process: trueNow 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:
@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:
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttpDefine your metrics at the package level:
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:
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:
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:
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
_totalsuffix: 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
serviceorapplabel 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 usesprometheus/client_golangand 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
_totalfor counters,_secondsfor 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.






Comments
Loading comments…