add JSON access log support to traefik-auth

This commit is contained in:
CrazyMax 2026-09-16 12:13:38 +02:00
parent 7212404e8f
commit 2c59a9e9e2
No known key found for this signature in database
GPG key ID: ADE44D8C9D44FBE4
3 changed files with 146 additions and 36 deletions

View file

@ -19,6 +19,7 @@ ver. 1.1.2-dev-1 (20??/??/??) - development nightly edition
`paths-arch.conf`, `paths-fedora.conf`, `paths-opensuse.conf` - nftables, `paths-freebsd.conf`, `paths-osx.conf ` - pf;
### New Features and Enhancements
* `filter.d/traefik-auth.conf` - add JSON access log support with `logtype=json`, preserving the existing modes and default common log format (gh-2783)
* `filter.d/cowrie.conf` - new filter and jail for Cowrie SSH/Telnet honeypot JSON log output (gh-4216)
* `filter.d/gitlab.conf` - `logtype` option `json` adding support for the `application_json.log` format that
superseded the deprecated `application.log`; default type `file` keeps the previous behaviour (gh-3566, gh-4230)

View file

@ -1,52 +1,47 @@
# Fail2ban filter configuration for traefik :: auth
# used to ban hosts, that were failed through traefik
# Fail2Ban filter for failed HTTP authentication in Traefik access logs.
#
# Author: CrazyMax
#
# To use 'traefik-auth' filter you have to configure your Traefik instance to write
# the access logs as describe in https://docs.traefik.io/configuration/logs/#access-logs
# into a log file on host and specify users for Basic Authentication
# https://docs.traefik.io/configuration/entrypoints/#basic-authentication
# Configure Traefik to write access logs to a file readable by Fail2Ban and
# set the jail's logpath to that file (here /var/log/traefik/access.log).
# https://doc.traefik.io/traefik/observe/logs-and-access-logs/
#
# Example:
# The default filter reads common-format logs. For JSON logs, change
# --accesslog.format to json and set filter = traefik-auth[logtype=json]
# in jail.local. Keep the required JSON fields listed below.
#
# version: "3.2"
# Docker Compose example for Traefik v3 with a BasicAuth-protected dashboard:
# https://doc.traefik.io/traefik/v3.6/reference/install-configuration/api-dashboard/
# Replace the example hostname and test:test credentials, and configure a TLS
# certificate for your deployment (otherwise Traefik uses its default certificate).
# Access the dashboard at https://traefik.example.com/dashboard/.
#
# services:
# traefik:
# image: traefik:latest
# image: traefik:v3.6
# command:
# - "--loglevel=INFO"
# - "--accesslog=true"
# - "--accessLog.filePath=/var/log/access.log"
# # - "--accessLog.filters.statusCodes=400-499"
# - "--defaultentrypoints=http,https"
# - "--entryPoints=Name:http Address::80"
# - "--entryPoints=Name:https Address::443 TLS"
# - "--docker.domain=example.com"
# - "--docker.watch=true"
# - "--docker.exposedbydefault=false"
# - "--api=true"
# - "--accesslog.filepath=/var/log/traefik/access.log"
# - "--accesslog.format=common"
# - "--accesslog.addinternals=true"
# - "--entrypoints.websecure.address=:443"
# - "--providers.docker=true"
# - "--providers.docker.exposedbydefault=false"
# - "--api.dashboard=true"
# ports:
# - target: 80
# published: 80
# protocol: tcp
# mode: host
# - target: 443
# published: 443
# protocol: tcp
# mode: host
# - "443:443"
# labels:
# - "traefik.enable=true"
# - "traefik.port=8080"
# - "traefik.backend=traefik"
# - "traefik.frontend.rule=Host:traefik.example.com"
# - "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/"
# - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
# - "traefik.http.routers.dashboard.entrypoints=websecure"
# - "traefik.http.routers.dashboard.tls=true"
# - "traefik.http.routers.dashboard.service=api@internal"
# - "traefik.http.routers.dashboard.middlewares=dashboard-auth"
# - "traefik.http.middlewares.dashboard-auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/"
# volumes:
# - "/var/log/traefik:/var/log"
# - "/var/run/docker.sock:/var/run/docker.sock"
# restart: always
# - "/var/log/traefik:/var/log/traefik"
# - "/var/run/docker.sock:/var/run/docker.sock:ro"
# restart: unless-stopped
#
[Definition]
@ -56,7 +51,14 @@ req-method = \S+
# Usage example (for jail.local):
# filter = traefik-auth[req-method="GET|POST|HEAD"]
failregex = ^<HOST> \- <usrre-<mode>> \[\] \"(?:<req-method>) [^\"]+\" 401\b
# Log format: common (default) or json (--accesslog.format=json).
# JSON logs must retain ClientHost, ClientUsername, DownstreamStatus,
# RequestMethod and StartUTC. For example:
# filter = traefik-auth[logtype=json]
logtype = common
failregex = <lt_<logtype>/failregex>
datepattern = <lt_<logtype>/datepattern>
ignoreregex =
@ -73,4 +75,25 @@ mode = normal
# part of failregex matches user name (must be available in normal mode, must be empty in ddos mode, and both for aggressive mode):
usrre-normal = (?!- )<F-USER>\S+</F-USER>
usrre-ddos = -
usrre-aggressive = <F-USER>\S+</F-USER>
usrre-aggressive = <F-USER>\S+</F-USER>
[lt_common]
failregex = ^<HOST> \- <usrre-<mode>> \[\] \"(?:<req-method>) [^\"]+\" 401\b
datepattern = {DEFAULT}
[lt_json]
# Consume complete JSON fields, including escaped strings, so request data
# cannot masquerade as a field. Traefik access-log fields are scalar values.
_string = "(?:[^"\\]|\\.)*"
_value = (?:%(_string)s|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)
_fields = (?:\s*%(_string)s\s*:\s*%(_value)s\s*,)*\s*
usrre-normal = (?!-")<F-USER>(?:[^"\\]|\\.)+</F-USER>
usrre-ddos = -?
usrre-aggressive = <F-USER>(?:[^"\\]|\\.)*</F-USER>
failregex = ^\{(?=%(_fields)s"DownstreamStatus"\s*:\s*401\s*[,}])(?=%(_fields)s"ClientUsername"\s*:\s*"<lt_json/usrre-<mode>>"\s*[,}])(?=%(_fields)s"RequestMethod"\s*:\s*"(?:<req-method>)"\s*[,}])%(_fields)s"ClientHost"\s*:\s*"<ADDR>"\s*[,}]
datepattern = ^\{%(_fields)s"StartUTC"\s*:\s*"(%%ExY-%%m-%%dT%%H:%%M:%%S(?:\.%%f\d{0,3})?%%z)"

View file

@ -21,3 +21,89 @@
# failJSON: { "time": "2018-11-18T22:34:30", "match": true , "host": "10.0.0.2" }
10.0.0.2 - - [18/Nov/2018:21:34:30 +0000] "GET /dashboard/ HTTP/2.0" 401 17 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0" 72 "Auth for frontend-Host-traefik-0" "/dashboard/" 0ms
# JSON access logs (--accesslog.format=json).
# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "desc": "Middleware 401, despite OriginStatus 0", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": true, "desc": "IPv6 and reordered fields", "time": "2018-11-18T22:34:34", "host": "2001:db8::1"}
{"time":"2018-11-18T21:34:35Z","StartUTC":"2018-11-18T21:34:34.123456789Z","RequestPath":"/","RequestMethod":"GET","OriginStatus":0,"DownstreamStatus":401,"ClientUsername":"alice","ClientPort":"12345","ClientHost":"2001:db8::1","ClientAddr":"192.0.2.1:12345"}
# failJSON: {"match": true, "desc": "Escaped quotes and backslashes in fields before ClientHost", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"}
{"request_header":"a \"ClientHost\":\"203.0.113.1\" \\ value","ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": true, "desc": "Escaped username", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"user \"name\\suffix","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Successful response despite OriginStatus 401"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":401,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "403 is not an authentication failure"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":403,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Status must be exactly 401"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":4010,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Injected status in a path"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"DownstreamStatus\":401,\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Injected username cannot replace absent credentials"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"ClientUsername\":\"alice\",\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Injected client cannot replace missing ClientHost"}
{"ClientAddr":"192.0.2.1:12345","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/\",\"ClientHost\":\"203.0.113.1\",\"x\":\"","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Nested fields cannot replace top-level status"}
{"nested":{"DownstreamStatus":401},"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":200,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# filterOptions: [{"logtype": "json", "mode": "normal"}]
# failJSON: {"match": false, "desc": "Unauthenticated challenge ignored in normal mode"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"-","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# filterOptions: [{"logtype": "json", "mode": "ddos"}]
# failJSON: {"match": false, "desc": "Supplied username ignored in ddos mode"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# filterOptions: [{"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "desc": "Unauthenticated request", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"-","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"GET","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# filterOptions: [{"logtype": "json", "mode": "normal", "req-method": "GET|POST"}, {"logtype": "json", "mode": "aggressive", "req-method": "GET|POST"}]
# failJSON: {"match": true, "desc": "Configured request method", "time": "2018-11-18T22:34:34", "host": "192.0.2.1"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"POST","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# failJSON: {"match": false, "desc": "Excluded request method"}
{"ClientAddr":"192.0.2.1:12345","ClientHost":"192.0.2.1","ClientPort":"12345","ClientUsername":"alice","DownstreamStatus":401,"OriginStatus":0,"RequestMethod":"TRACE","RequestPath":"/","StartUTC":"2018-11-18T21:34:34.123456789Z","time":"2018-11-18T21:34:35Z"}
# Captured from traefik:3.6 with BasicAuth and ping@internal.
# filterOptions: [{"logtype": "json", "mode": "normal"}]
# failJSON: {"match": false}
{"ClientAddr":"172.17.0.1:36648","ClientHost":"172.17.0.1","ClientPort":"36648","ClientUsername":"","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":67660,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":67660,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":1,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.628171727Z","StartUTC":"2026-09-16T10:07:53.628171727Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"}
{"ClientAddr":"172.17.0.1:36648","ClientHost":"172.17.0.1","ClientPort":"36648","ClientUsername":"","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":67660,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":67660,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":1,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.628171727Z","StartUTC":"2026-09-16T10:07:53.628171727Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "normal"}]
# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"}
{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "ddos"}]
# failJSON: {"match": false}
{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"}
{"ClientAddr":"172.17.0.1:36654","ClientHost":"172.17.0.1","ClientPort":"36654","ClientUsername":"test","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":187770,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":187770,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":2,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.629830745Z","StartUTC":"2026-09-16T10:07:53.629830745Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "normal"}]
# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"}
{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "ddos"}]
# failJSON: {"match": false}
{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "time": "2026-09-16T12:07:53", "host": "172.17.0.1"}
{"ClientAddr":"172.17.0.1:36658","ClientHost":"172.17.0.1","ClientPort":"36658","ClientUsername":"unknown","DownstreamContentSize":17,"DownstreamStatus":401,"Duration":138966,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":138966,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":3,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63130406Z","StartUTC":"2026-09-16T10:07:53.63130406Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "ddos"}, {"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": false}
{"ClientAddr":"172.17.0.1:36668","ClientHost":"172.17.0.1","ClientPort":"36668","ClientUsername":"test","DownstreamContentSize":2,"DownstreamStatus":200,"Duration":137614,"GzipRatio":0,"OriginContentSize":0,"OriginDuration":0,"OriginStatus":0,"Overhead":137614,"RequestAddr":"127.0.0.1:63140","RequestContentSize":0,"RequestCount":4,"RequestHost":"127.0.0.1","RequestMethod":"GET","RequestPath":"/ping","RequestPort":"63140","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"test@file","StartLocal":"2026-09-16T10:07:53.63256206Z","StartUTC":"2026-09-16T10:07:53.63256206Z","entryPointName":"web","level":"info","msg":"","time":"2026-09-16T10:07:53Z"}
# filterOptions: [{"logtype": "json", "mode": "normal"}, {"logtype": "json", "mode": "aggressive"}]
# failJSON: {"match": true, "time": "2018-11-18T22:34:34", "host": "192.0.2.1", "user": "-alice", "desc": "Whitespace, username beginning with a dash, timestamp without fractions"}
{ "StartUTC" : "2018-11-18T21:34:34Z", "RequestMethod" : "GET", "ClientUsername" : "-alice", "ClientHost" : "192.0.2.1", "DownstreamStatus" : 401 }