# Logging in Zig ESP-IDF Applications: Implementation Guide

> Learn to implement logging in Zig ESP-IDF applications. This guide shows how to route std log calls to the ESP-IDF subsystem for colorized, hardware-optimized output.

- Repository: [Matheus C. França/zig-esp-idf-sample](https://github.com/kassane/zig-esp-idf-sample)
- Tags: how-to-guide
- Published: 2026-03-05

---

**Zig ESP-IDF applications route `std.log` calls through a custom bridge in `imports/logger.zig` to forward messages to the ESP-IDF logging subsystem, enabling colorized, hardware-optimized output.**

Logging in Zig ESP-IDF applications bridges the Zig standard library's `std.log` interface with the native ESP-IDF capabilities. The `kassane/zig-esp-idf-sample` repository implements this integration via a thin wrapper that maps Zig log levels to ESP-IDF categories while preserving compile-time optimizations. This architecture allows developers to use idiomatic Zig logging patterns while leveraging the ESP32's dedicated logging hardware.

## Architecture Overview

The logging stack consists of a Zig-side adapter that translates standard library calls into ESP-IDF format.

### Core Components

The integration centers on **`imports/logger.zig`**, which implements `espLogFn`—the function assigned to `std_options.logFn`. This module handles level translation, colorized prefixing, and final output to the ESP-IDF backend. The `idf.zig` file re-exports this functionality as `idf.log`, providing a clean import path for application code.

The ESP-IDF sys bindings, generated in `build.zig`, expose low-level C APIs including `esp_log_write` and the `esp_log_level_t` enum. These symbols are imported in `logger.zig` via `@import("sys")`, enabling direct calls to the underlying ESP-IDF logging implementation.

### Log Flow Pipeline

When application code calls `std.log.info("Wi-Fi connected", .{})`, the following sequence occurs:

1. The Zig runtime checks the global `std_options` struct (as configured in `main/examples/wifi-station.zig` at lines 46-52) and routes the call to `idf.log.espLogFn`
2. `espLogFn` (lines 6-21 in `imports/logger.zig`) maps the Zig log level to an ESP-IDF level using `levelToEsp`
3. The function selects an ANSI color code via `levelColor` and builds a prefix like `"[INFO] (espressif): "`
4. The `ESP_LOG` helper (lines 56-70) determines at compile-time whether format arguments are known:
   - If comptime-known, it uses `std.fmt.comptimePrint` for zero-allocation formatting
   - Otherwise, it allocates a temporary buffer for runtime formatting
5. Finally, `sys.esp_log_write(level, tag, "%s", buffer.ptr)` forwards the message to the ESP-IDF console

## Configuring std_options for ESP-IDF Logging

To enable logging in your application, configure the global `std_options` struct to use the ESP-IDF bridge. As demonstrated in `main/examples/wifi-station.zig` and `main/app.zig`, define this at the namespace level:

```zig
const std = @import("std");
const idf = @import("idf");

pub const std_options: std.Options = .{
    .log_level = .debug,
    .logFn = idf.log.espLogFn,
};

pub fn main() void {
    std.log.info("Application started", .{});
    std.log.warn("Low memory warning", .{});
    std.log.err("Fatal error – rebooting!", .{});
}

```

This configuration routes all `std.log.*` calls through the ESP-IDF backend, automatically applying colorized prefixes and hardware-optimized output handling.

## Custom Log Scopes and Tags

You can override the default `"espressif"` tag (defined in `imports/logger.zig` at line 4) by passing a custom enum literal to the low-level wrapper:

```zig
pub fn initSensor() void {
    // Directly call the low-level wrapper for fine-grained control
    idf.log.espLogFn(.info, .sensor, "sensor initialized: id={}", .{123});
}

```

This produces output prefixed with `[INFO] (sensor):` instead of the default `[INFO] (espressif):`, enabling fine-grained log filtering by component.

## Compile-Time Log Level Optimization

The default log level adapts to the build mode. In `imports/logger.zig` (lines 26-30), the implementation selects `ESP_LOG_DEBUG` for Debug builds and `ESP_LOG_INFO` or `ESP_LOG_ERROR` for Release builds.

For conditional logging that disappears entirely in release builds:

```zig
inline fn debugPrint(comptime msg: []const u8) void {
    if (@import("builtin").mode == .Debug) {
        std.log.debug(msg, .{});
    }
}

```

When `std_options.log_level` excludes debug messages, the compiler eliminates these calls entirely, resulting in zero runtime overhead.

## Colorization and Level Mapping

The bridge automatically adds ANSI color codes defined in `imports/logger.zig` (such as `LOG_COLOR_RED`, `LOG_COLOR_GREEN`, etc.) to log prefixes. The `levelColor` function assigns:
- **Red** for errors (`ESP_LOG_ERROR`)
- **Yellow** for warnings (`ESP_LOG_WARN`)
- **Green** for info (`ESP_LOG_INFO`)
- **Default** for debug (`ESP_LOG_DEBUG`)

These colors map to the underlying ESP-IDF level enum values through `levelToEsp`, ensuring that the ESP-IDF console displays Zig log levels with appropriate visual formatting and severity filtering.

## Summary

- The `imports/logger.zig` file provides `espLogFn`, which bridges `std.log` to ESP-IDF's `esp_log_write`
- Configure the global `std_options` struct with `.logFn = idf.log.espLogFn` to enable the integration, as shown in `main/app.zig` and example files
- Log levels automatically map between Zig's standard library and ESP-IDF's native `esp_log_level_t` enum values via the `levelToEsp` function
- Colorized output uses ANSI codes (like `LOG_COLOR_RED`) defined in the logger module for improved readability
- Compile-time formatting via `std.fmt.comptimePrint` avoids allocations when log arguments are known at build time

## Frequently Asked Questions

### How do I change the default log level in Zig ESP-IDF applications?

Modify the `log_level` field in your `std_options` configuration. Set `.log_level = .info` to show only info, warning, and error messages, or `.log_level = .debug` to enable all output including debug traces. The ESP-IDF backend will filter messages accordingly at runtime based on the level mapping in `imports/logger.zig`.

### How does the logging bridge handle different log levels between Zig and ESP-IDF?

The `levelToEsp` function in `imports/logger.zig` translates Zig's `.err`, `.warn`, `.info`, and `.debug` levels to corresponding ESP-IDF levels (`ESP_LOG_ERROR`, `ESP_LOG_WARN`, `ESP_LOG_INFO`, `ESP_LOG_DEBUG`). This ensures that ESP-IDF's built-in filtering and routing mechanisms work correctly with Zig log calls.

### Can I use custom tags for specific modules in Zig ESP-IDF logging?

Yes. While the default scope is `.espressif` (set at line 4 of `imports/logger.zig`), you can pass a custom enum literal as the scope parameter to `idf.log.espLogFn`. For example, using `.sensor` creates logs tagged with `(sensor)` instead of `(espressif)`, allowing you to filter output by component using standard ESP-IDF logging tools.

### Does logging impact binary size in release builds?

No. When you set `std_options.log_level` to exclude debug messages (typically `.info` or higher for release builds), the Zig compiler eliminates debug log calls entirely at compile time. Additionally, the `ESP_LOG` implementation uses `std.fmt.comptimePrint` for static strings, avoiding runtime allocations and minimizing flash usage.