Files
smallwebwaf/internal/requestlog/requestlog.go
T
clawbot c15c329dce
check / check (push) Failing after 2s
The image apps build FROM, with its health check (closes #45)
The Dockerfile's last stage is now the image of "Deployment" in SPEC.md:
Ubuntu 26.04 with ca-certificates, nix-bin and runit from a dated
snapshot whose InRelease files are checked by hash, nixpkgs from its
release file checked by SHA-256, runsvinit built at a fixed commit, and
smallwebwaf as a runit service. smallwebwaf answers
/_smallwebwaf/healthz, and `smallwebwaf healthcheck`, which takes no
further argument, is the image's HEALTHCHECK. script/example-app builds
an app on the image and checks it end to end.

The Nix profile comes last on the PATH: first, busybox from nixpkgs
replaced runit's own runsvdir and sv. SPEC.md is corrected to match
what was built.

Model: opus-5-5
2026-10-04 07:51:03 +00:00

115 lines
3.8 KiB
Go

// Package requestlog writes the lines smallwebwaf prints on stdout: one
// JSON object per request, marked "type":"request", and the process's own
// messages as JSON lines marked "type":"process".
package requestlog
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"time"
)
// The action a request line names: what smallwebwaf did with the
// request.
const (
// ActionForward is a request passed to the app.
ActionForward = "forward"
// ActionTooLarge is a request or response over its size limit.
ActionTooLarge = "too_large"
// ActionTimedOut is a request or response that ran out of time.
ActionTimedOut = "timed_out"
// ActionUpstreamError is a request the app could not be reached
// for, or whose answer could not be passed on.
ActionUpstreamError = "upstream_error"
// ActionRateLimited is a request refused because it took its client
// over a rate limit, or came while the client was over one.
ActionRateLimited = "rate_limited"
// ActionCountryDenied is a request refused for its client's country.
ActionCountryDenied = "country_denied"
// ActionAdmin is a request smallwebwaf answered at one of its own
// endpoints, under /_smallwebwaf/.
ActionAdmin = "admin"
)
// timeLayout is RFC 3339 with milliseconds.
const timeLayout = "2006-01-02T15:04:05.000Z07:00"
// Line is one request's line in the request log. The field names are
// those of the "Request log" section of SPEC.md.
//
//nolint:tagliatelle // SPEC.md's request log names its fields in snake_case
type Line struct {
Type string `json:"type"`
Time string `json:"time"`
ClientIP string `json:"client_ip"`
PeerIP string `json:"peer_ip"`
Country string `json:"country"`
Method string `json:"method"`
Host string `json:"host"`
Path string `json:"path"`
Query string `json:"query"`
Protocol string `json:"protocol"`
Status int `json:"status"`
UpstreamStatus int `json:"upstream_status,omitempty"`
RequestBytes int64 `json:"request_bytes"`
ResponseBytes int64 `json:"response_bytes"`
Referer string `json:"referer"`
UserAgent string `json:"user_agent"`
Action string `json:"action"`
// LimitHit is the window whose rate limit the request went over:
// minute, hour or day.
LimitHit string `json:"limit_hit,omitempty"`
// Aborted is true when the client went away early.
Aborted bool `json:"aborted,omitempty"`
// DurationTotal and DurationUpstreamTotal are in milliseconds.
DurationTotal float64 `json:"duration_total"`
DurationUpstreamTotal float64 `json:"duration_upstream_total,omitempty"`
}
// Write writes line to w as one JSON line marked "type":"request".
func Write(w io.Writer, line *Line) error {
line.Type = "request"
encoded, err := json.Marshal(line)
if err != nil {
return fmt.Errorf("encode the request log line: %w", err)
}
_, err = w.Write(append(encoded, '\n'))
if err != nil {
return fmt.Errorf("write the request log line: %w", err)
}
return nil
}
// FormatTime formats t for a line's time field: RFC 3339 in UTC, with
// milliseconds.
func FormatTime(t time.Time) string {
return t.UTC().Format(timeLayout)
}
// Milliseconds is d in milliseconds, to the microsecond.
func Milliseconds(d time.Duration) float64 {
return float64(d.Microseconds()) / float64(time.Millisecond/time.Microsecond)
}
// NewProcessLogger returns the logger for the process's own messages:
// JSON lines on w, marked "type":"process", with the time in the same form
// as a request line's.
func NewProcessLogger(w io.Writer) *slog.Logger {
handler := slog.NewJSONHandler(w, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
if attr.Key == slog.TimeKey && len(groups) == 0 {
return slog.String(slog.TimeKey, FormatTime(attr.Value.Time()))
}
return attr
},
})
return slog.New(handler).With("type", "process")
}