How to Integrate spdlog with Grafana Loki: A Complete C++ Guide

You can integrate spdlog with Grafana Loki by using the built-in loki_sink which formats logs as JSON and pushes them via HTTP to Loki's /loki/api/v1/push endpoint.

The gabime/spdlog repository provides a dedicated Loki sink (loki_sink.h) that enables direct log streaming from C++ applications to Grafana Loki without requiring additional agents or forwarders. This sink uses the lightweight cpp-httplib client to transmit structured log data, making integration straightforward for modern C++ projects.

Understanding the Loki Sink Architecture

The Loki sink implementation in include/spdlog/sinks/loki_sink.h handles the entire communication pipeline from log generation to HTTP transmission. Understanding this architecture helps you configure the integration correctly for your throughput requirements.

HTTP Client and Configuration

The sink constructs a httplib::Client instance during initialization to manage connections to your Loki server. Configuration is controlled through the loki_sink_config struct defined at lines 40-48, which specifies the Loki server host, port, optional static labels, and connection timeout settings.

Message Formatting and Payload Construction

By default, the sink uses the pattern "%v" (message text only) because Loki stores timestamps and severity levels as separate structured metadata. When the sink_it_ method processes a log entry, it constructs a JSON payload where the timestamp (nanoseconds since epoch) and formatted message populate the values array. This construction logic resides at lines 96-113 of the header file.

The HTTP Push Mechanism

Each log entry triggers an HTTP POST request to /loki/api/v1/push with Content-Type: application/json. The implementation at lines 78-88 handles the synchronous transmission, throwing an exception if the Loki server returns an error status code.

Configuring the Loki Sink

Create a loki_sink_config object to define your Loki endpoint and static labels:

#include <spdlog/sinks/loki_sink.h>

spdlog::sinks::loki_sink_config cfg{
    "localhost",    // Loki host
    3100            // Loki HTTP port
};
cfg.labels = { {"app", "my_service"}, {"env", "production"} };
cfg.add_level_label = true;   // Automatically adds log level as a label
cfg.timeout_seconds = 5;      // Blocking timeout for HTTP operations

The labels map defines static key-value pairs attached to every log entry, enabling efficient filtering in Grafana. Setting add_level_label to true creates a dynamic label from the log level (info, warn, error).

Choosing Sync vs Async Loggers

The repository exposes three factory functions in loki_sink.h:

  • loki_logger_mt: Thread-safe synchronous logger
  • loki_logger_st: Single-threaded synchronous logger
  • loki_logger_async_mt: Asynchronous logger with background thread (recommended)

Because each sink_it_ call performs a blocking HTTP request, the asynchronous variant is essential for production workloads. As noted in the source comments at lines 15-16, synchronous sinks suit only low-throughput scenarios or debugging environments.

Complete Integration Example

The following example demonstrates a production-ready setup using the asynchronous logger:

#include <spdlog/spdlog.h>
#include <spdlog/sinks/loki_sink.h>

int main() {
    // 1. Configure connection to Loki
    spdlog::sinks::loki_sink_config cfg{
        "localhost",
        3100
    };
    cfg.labels = { {"app", "my_service"}, {"env", "prod"} };
    cfg.add_level_label = false;
    cfg.timeout_seconds = 5;

    // 2. Create async logger (lines 126-129 in loki_sink.h)
    auto logger = spdlog::loki_logger_async_mt("loki_logger", cfg);

    // 3. Generate logs - automatically pushed to Loki
    logger->info("Service started");
    logger->warn("Cache miss for key={}", "user:123");
    logger->error("Database connection failed: {}", "timeout");

    // 4. Flush on shutdown (optional, automatic in destructor)
    logger->flush();
}

The loki_logger_async_mt factory function (lines 126-129) creates an asynchronous multi-threaded logger that internally uses loki_sink_mt with a dedicated background thread, ensuring network I/O never blocks your application logic.

Performance Considerations

The synchronous Loki sink executes HTTP requests on the calling thread, making it suitable only for development or low-volume logging. For high-throughput production systems, always use loki_logger_async_mt, which offloads network operations to a background thread through the async mechanism defined in include/spdlog/async.h.

Each HTTP request includes the full JSON payload, so batching behavior depends on your spdlog async queue settings. Ensure your spdlog::init_thread_pool() configuration provides sufficient queue capacity to handle traffic spikes without dropping messages.

Summary

  • Primary Header: Include include/spdlog/sinks/loki_sink.h to access the Loki integration.
  • Configuration: Use loki_sink_config to set host, port, static labels, and timeouts.
  • Async Requirement: Use loki_logger_async_mt for production to prevent HTTP blocking.
  • Payload Format: The sink automatically formats entries as Loki-compatible JSON with nanosecond timestamps.
  • Endpoint: Logs push to /loki/api/v1/push via HTTP POST with application/json content type.

Frequently Asked Questions

Does spdlog require external dependencies for Loki integration?

No additional dependencies are required beyond the standard spdlog headers. The loki_sink implementation bundles the cpp-httplib client directly in the header file at lines 6-14, providing a self-contained HTTP transport layer without requiring separate library linking or package installation.

What is the difference between loki_logger_mt and loki_logger_async_mt?

The loki_logger_mt factory creates a synchronous logger where the calling thread blocks during HTTP transmission to Loki. The loki_logger_async_mt factory creates an asynchronous logger that queues messages and processes HTTP requests on a background thread, preventing network latency from impacting application performance as recommended in the source comments at lines 15-16.

How do I add custom labels to my Loki logs?

Set the labels field in your loki_sink_config object to a map of string key-value pairs before constructing the logger. These static labels attach to every log entry sent to Loki. Additionally, enable add_level_label to automatically include the log severity (info, warn, error) as a dynamic label for filtering in Grafana.

Why does the Loki sink use the %v pattern by default?

The sink defaults to the "%v" pattern (message text only) because Loki stores timestamps and log levels as separate structured metadata fields in the JSON payload. Including timestamp or level information in the message body would create redundant data, as these values are already transmitted in the values array alongside the message text according to the Loki push API specification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →