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

> Easily integrate spdlog with Grafana Loki using the built-in loki_sink. Format logs as JSON and push directly to Loki for streamlined C++ application monitoring. Get started today!

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: how-to-guide
- Published: 2026-07-19

---

**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](https://github.com/gabime/spdlog) repository provides a dedicated **Loki sink** ([`loki_sink.h`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/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:

```cpp
#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`](https://github.com/gabime/spdlog/blob/main/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`](https://github.com/gabime/spdlog/blob/main/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.