HAProxy’s default log format is a relic from a time when logs were primarily for human eyes, meaning you’re probably wrestling with unstructured text when you should be querying structured data.

Let’s see HAProxy in action, spitting out logs that are actually useful for machines. Imagine a busy web server. We’ll configure HAProxy to log key request details in a JSON format that’s immediately parsable by log aggregation tools like Splunk, Elasticsearch, or Loki.

frontend http_frontend
    bind *:80
    mode http
    default_backend http_backend
    option httplog

    log-format '{ "timestamp": "%{+yyyy-MM-dd'T'HH:mm:ss.SSS}t", "client_ip": "%ci", "client_port": "%cp", "backend_ip": "%bi", "backend_port": "%bp", "frontend_name": "%f", "backend_name": "%b", "http_method": "%HM", "url": "%U", "http_version": "%HV", "status_code": "%ST", "bytes_sent": "%B", "bytes_received": "%R", "request_time": "%Tr", "session_time": "%Ts", "error_code": "%rc", "ssl_cipher": "%V", "ssl_protocol": "%k", "capture_vars": {%{ capture.var1 }:%{ capture.var2 }} }'


backend http_backend
    balance roundrobin
    server s1 192.168.1.10:8080
    server s2 192.168.1.11:8080

This configuration defines a frontend named http_frontend listening on port 80, directing traffic to a backend named http_backend. The option httplog directive enables HTTP logging, and the log-format directive is where the magic happens. We’re specifying a JSON structure with key-value pairs.

Here’s what each field in our log-format means:

  • timestamp: The precise time the log entry was generated. %{+yyyy-MM-dd'T'HH:mm:ss.SSS}t is a powerful format specifier for ISO 8601 timestamps.
  • client_ip: The IP address of the client making the request (%ci).
  • client_port: The port of the client (%cp).
  • backend_ip: The IP address of the backend server that handled the request (%bi).
  • backend_port: The port of the backend server (%bp).
  • frontend_name: The name of the frontend that accepted the connection (%f).
  • backend_name: The name of the backend server pool (%b).
  • http_method: The HTTP request method (GET, POST, etc.) (%HM).
  • url: The requested URL path (%U).
  • http_version: The HTTP protocol version (HTTP/1.1, HTTP/2) (%HV).
  • status_code: The HTTP status code returned by the backend (200, 404, 500) (%ST).
  • bytes_sent: The number of bytes sent to the client (%B).
  • bytes_received: The number of bytes received from the client (%R).
  • request_time: The time taken to process the request on the backend, in milliseconds (%Tr).
  • session_time: The total time for the entire session, in milliseconds (%Ts).
  • error_code: HAProxy’s internal error code if the request failed before reaching the backend (%rc).
  • ssl_cipher: The SSL/TLS cipher used for HTTPS connections (%V).
  • ssl_protocol: The SSL/TLS protocol used (%k).
  • capture_vars: This is a bit more advanced. It allows you to capture specific parts of the request or response, like headers. Here, we’re trying to capture var1 and var2. You’d need to configure these with capture request header var1 len 32 (or similar) in the frontend or backend.

When a request comes in, HAProxy will generate a log line like this:

{ "timestamp": "2023-10-27T10:30:05.123", "client_ip": "192.168.1.100", "client_port": "54321", "backend_ip": "192.168.1.10", "backend_port": "8080", "frontend_name": "http_frontend", "backend_name": "http_backend", "http_method": "GET", "url": "/api/users", "http_version": "HTTP/1.1", "status_code": 200, "bytes_sent": 1500, "bytes_received": 300, "request_time": 55, "session_time": 75, "error_code": 0, "ssl_cipher": "-", "ssl_protocol": "-", "capture_vars": { "var1":"value1", "var2":"value2" } }

This structured format makes it trivial to search for requests that took longer than 100ms (request_time > 100), or to count the number of 500 errors originating from a specific backend server.

The most surprising thing about HAProxy’s log-format is its ability to dynamically inject captured HTTP headers or other request attributes directly into your log message, transforming what could be a generic log line into a highly specific audit trail for individual requests, without needing to parse the raw HTTP body.

If you find yourself needing to log specific request headers that aren’t standard fields, you’ll need to use the capture request header <name> len <length> directive within your frontend or backend configuration. For example, to capture a custom X-Request-ID header, you’d add capture request header X-Request-ID len 32 to your frontend. Then, you can reference it in log-format as %{ req.hdr(X-Request-ID) }.

Want structured learning?

Take the full Haproxy course →