DevOps and Infrastructure

Building Custom Prometheus Exporters for Legacy Systems

Modern infrastructure relies heavily on metrics, but a significant portion of enterprise critical path functionality still runs on legacy applications. These systems—often built decades ago—are frequently opaque, lacking native API endpoints for modern observability tools. This gap creates blind spots that can lead to unexpected outages and performance degradation. In this guide, we will explore how to bridge this gap by building custom Prometheus exporters. By wrapping legacy logic in a metrics layer, you can expose vital health indicators to your existing Grafana and Prometheus dashboards without modifying the core application code.

Understanding the Exporter Pattern

A Prometheus exporter is a separate service that exposes metrics in a format Prometheus can scrape. Unlike the application itself, which may be written in COBOL, Delphi, or an outdated version of Java, the exporter acts as a thin wrapper. It interacts with the legacy system via available interfaces—such as SNMP, HTTP APIs, or database queries—and translates those responses into Prometheus-compatible exposition format.

This separation of concerns is crucial. It allows you to manage the health of the legacy app independently from the application logic. If the exporter fails, your monitoring stack remains stable. If the legacy app fails, the exporter simply reports zero or error states, alerting your team before users do.

Choosing the Right Language

When building an exporter, the choice of programming language often dictates the ease of maintenance and performance. Two languages dominate this space: Go and Python.

Go is the industry standard for writing exporters. The official prometheus/client_golang library is robust, type-safe, and compiles to a single static binary, making deployment trivial in containerized environments. It is highly performant and handles concurrency well.

Python is excellent for rapid prototyping, especially if your team is already familiar with data manipulation libraries like Pandas for metric aggregation. The prometheus_client package is simple to use but can be slower in high-throughput scenarios.

Implementation Example with Go

Let’s look at a practical example using Go. Suppose you have a legacy inventory database that stores stock levels. You want to expose the count of low-stock items. Here is how you might structure the exporter:

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// LowStockCollector implements the prometheus.Collector interface.
type LowStockCollector struct {
    LowStockCount *prometheus.Desc
}

// NewLowStockCollector initializes a new collector.
func NewLowStockCollector() *LowStockCollector {
    return &LowStockCollector{
        LowStockCount: prometheus.NewDesc(
            "legacy_inventory_low_stock",
            "Count of items with low stock in legacy database",
            nil, nil,
        ),
    }
}

// Describe sends the descriptors of each Metric related to this collector.
func (c *LowStockCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- c.LowStockCount
}

// Collect is called by the Prometheus registry when collecting metrics.
func (c *LowStockCollector) Collect(ch chan<- prometheus.Metric) {
    // Simulate fetching data from legacy system
    count := fetchLowStockCountFromLegacyDB()
    ch <- prometheus.MustNewConstMetric(
        c.LowStockCount,
        prometheus.GaugeValue,
        float64(count),
    )
}

func main() {
    registry := prometheus.NewRegistry()
    collector := NewLowStockCollector()
    registry.MustRegister(collector)

    http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
    log.Fatal(http.ListenAndServe(":9100", nil))
}

func fetchLowStockCountFromLegacyDB() int {
    // Logic to query legacy DB goes here
    return 42
}

In this example, we define a custom collector that implements the Describe and Collect methods. The Collect method is where you interact with the legacy system. By exposing this on /metrics, Prometheus can scrape the data without needing to modify the legacy application.

Handling Authentication and Security

Legacy systems often lack modern authentication mechanisms. However, the exporter itself should be secured. Since the exporter is a separate service, you can place it behind a reverse proxy like Nginx or Apache, adding basic authentication or mTLS. This ensures that even if the legacy interface is unsecured, the metrics endpoint remains protected from unauthorized access.

Conclusion

Extending observability to legacy applications is not just a technical necessity; it is a business imperative. By building custom Prometheus exporters, you gain visibility into systems that would otherwise remain black boxes. Whether you choose Go for performance or Python for simplicity, the key is to keep the exporter lightweight and focused. This approach allows you to gradually modernize your monitoring stack while maintaining the stability of your critical legacy infrastructure. Start small, expose one metric, and expand from there.

Share: