How to filter out requests with status 200 from log

1. The problem I’m having:

Hi there!
I’m using Caddy mainly as a reverse proxy for my services running in containers.
I’m splitting my logging for each subdomain to have finer grained fail2ban filtering available.
Works great so far, but Silverbullet is sending A LOT of requests (Log grows to several MB in a couple of minutes), even while idling - logrotate is fine and all, but i also want to limit ssd-wear.

My wish is to filter out Status 200 out of the logs, but so far i haven’t had any luck with any method or hint i found.

My current attempt looks like this:

# general Logging
{
    log {
        output file /var/log/caddy/access.log {
            roll_size 10MB
            roll_keep 10
        }
        format json
    }
}
# logging for subdomain/service
silverbullet.saldorin.duckdns.org {
        reverse_proxy localhost:3000 {
                header_up X-Real-IP {remote_host}
        }

        @not200 not expression `{err.status_code} != 200`

        log_skip @not200
        log {
                output file /var/log/caddy/silverbullet.access.log
        }
}

Unfortunately, this still logs status 200 requests…
Hopefully, someone can point me in the right direction? :slight_smile:

2. Error messages and/or full log output:

No related output in the general log. Example log of Silverbullet-request:

{"level":"info","ts":1750066419.574309,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"192.168.1.1","remote_port":"54714","client_ip":"192.168.1.1","proto":"HTTP/3.0","method":"GET","host":"silverbullet.saldorin.duckdns.org","uri":"/service_worker.js","headers":{"Service-Worker":["script"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Dnt":["1"],"Sec-Fetch-Site":["same-origin"],"Cache-Control":["max-age=0"],"Sec-Fetch-Mode":["same-origin"],"Priority":["u=4"],"Accept":["*/*"],"Alt-Used":["silverbullet.saldorin.duckdns.org"],"Sec-Fetch-Dest":["serviceworker"],"Accept-Language":["en-US,en;q=0.5"],"User-Agent":["Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h3","server_name":"silverbullet.saldorin.duckdns.org"}},"bytes_read":0,"user_id":"","duration":0.003773312,"size":18175,"status":200,"resp_headers":{"Via":["1.1 Caddy"],"Cache-Control":["no-cache"],"Vary":["Accept-Encoding"],"Content-Type":["application/javascript"],"Content-Encoding":["br"],"Content-Length":["18175"],"Date":["Mon, 16 Jun 2025 09:33:39 GMT"]}}

3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

a. System environment:

Debian 12 x86 VM running atop Proxmox, managed by systemd, as described in Install — Caddy Documentation

b. Command:

systemctl start caddy.service

c. Service/unit/compose file:

# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

# The Caddyfile is an easy way to configure your Caddy web server.
#
# Unless the file starts with a global options block, the first
# uncommented line is always the address of your site.
#
# To use your own domain name (with automatic HTTPS), first make
# sure your domain's A/AAAA DNS records are properly pointed to
# this machine's public IP, then replace ":80" below with your
# domain name.


# Create log that is parceable for fail2ban
{
    log {
        output file /var/log/caddy/access.log {
            roll_size 10MB
            roll_keep 10
        }
        format json
    }
}

# Setup DNS API
*.saldorin.duckdns.org {
        tls {
                dns duckdns {
                        token xxx
                }
        }
}

# Setup base dir
 :80, :443 {
        respond /health "Im healthy" 200

        log
}

# Refer to the Caddy docs for more information:
# https://caddyserver.com/docs/caddyfile


silverbullet.saldorin.duckdns.org{
        reverse_proxy localhost:3000 {
                header_up X-Real-IP {remote_host}
        }

        @not200 not expression `{err.status_code} != 200`

        log_skip @not200
        log {
                output file /var/log/caddy/silverbullet.access.log
        }
}

[other services...]

5. Links to relevant resources:

Some posts/suggestions that i tried to implement/used for further research:

https://caddy.community/t/exclude-certain-requests-from-logs-based-on-request-path/15607
https://caddy.community/t/how-to-disable-logs-with-a-matcher/16882 (max 4 links allowed :wink: )

Relevant issue (i think?):

silverbullet.saldorin.duckdns.org {
    reverse_proxy localhost:3000 {
        header_up X-Real-IP {remote_host}

        @200 {
            status 200
        }
        handle_response @200 {
            log_skip
        }
    }

    log {
        output file /var/log/caddy/silverbullet.access.log
    }
}
1 Like

Ah, i had tried that handle_response (with a slightly different matcher), but not inside the reverse_proxy statement i think? Although that is indeed what my second linked thread suggested…oh well…

With your suggestion, it doesn’t quite work, as silverbullet doesn’t work anymore (curiously, i get 308 with curl that is not logged and 404 with a browser, that IS logged..), but when including copy_response in the handle_response section, it finally excludes 200 from the logs :partying_face:
Thank you!

Now that the log isn’t as cluttered, i see that there are quite some 304s as well - it seems like the matcher works as an OR-statement by default?
From a quick test, it seems like

 @200or304 {
            status 200
            status 304
        }

catches both 200 and 304 responses - i would have assumed that these matchers behave more like AND-statements :thinking:
Or is there something i’m missing with this?

Same matchers are OR’ed, different matchers are AND’ed.

@or1 {
    status 200 304
}

@or2 {
    status 200
    status 304
}

@or3 {
    header key1 value1
    header key1 value2
}

@and1 {
    header key1 value1
    header key2 value2
}

@and2 {
    method POST
    path /somepath
}

etc.

2 Likes

Ah got it - kind of makes sense :smiley:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.