detached 6b418bd

refactor: move access log port to boundaries/out, fix hex arch violations

- Move AccessLogEntry and AccessLogWriter interface to internal/boundaries/out/
  (driven-port interfaces belong in the ports layer, not in adapters)
- Move HealthCheckUserAgentPrefix constant to boundaries/out/accesslog.go
  (was in domain/ which is wrong; it's an HTTP infrastructure detail)
- Update adapters/out/accesslog to implement the boundary interface
- Update middleware/accesslog.go to depend on the port, not the output adapter
- Update app/run.go to use out.AccessLogWriter instead of middleware.AccessLogWriter
- Fixes adapter-to-adapter coupling (input adapter importing output adapter)
commit
6b418bd1c15c599cf3cf86dbc806094d049bd0db
author
brice · · 2026-04-03 17:44 UTC
parent
7121811
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +69 -56
M internal/adapters/in/http/middleware/accesslog.go +4 -11
@@ -8,16 +8,9 @@ import (
88
99 "github.com/bnema/zerowrap"
1010
11- "github.com/bnema/gordon/internal/adapters/out/accesslog"
12- "github.com/bnema/gordon/internal/domain"
11+ out "github.com/bnema/gordon/internal/boundaries/out"
1312 )
1413
15-// AccessLogWriter is the interface the middleware needs from the output adapter.
16-// *accesslog.Writer satisfies this interface.
17-type AccessLogWriter interface {
18- Write(entry accesslog.Entry) error
19-}
20-
2114 // AccessLogger is a middleware that writes one access-log entry per HTTP request
2215 // to the provided writer. It runs alongside (not instead of) RequestLogger.
2316 //
@@ -26,7 +19,7 @@ type AccessLogWriter interface {
2619 //
2720 // Write failures are reported as warnings through the application logger and
2821 // never fail the HTTP response.
29-func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap.Logger, trustedNets []*net.IPNet) func(http.Handler) http.Handler {
22+func AccessLogger(writer out.AccessLogWriter, excludeHealthChecks bool, log zerowrap.Logger, trustedNets []*net.IPNet) func(http.Handler) http.Handler {
3023 return func(next http.Handler) http.Handler {
3124 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3225 start := time.Now()
@@ -50,7 +43,7 @@ func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap
5043 // filter can never drift. Loopback check reuses localhostNets from
5144 // cidr.go (same package) — no separate loopback definition needed.
5245 if excludeHealthChecks {
53- if strings.HasPrefix(r.UserAgent(), domain.HealthCheckUserAgentPrefix) ||
46+ if strings.HasPrefix(r.UserAgent(), out.HealthCheckUserAgentPrefix) ||
5447 IsTrustedProxy(clientIP, localhostNets) {
5548 return
5649 }
@@ -61,7 +54,7 @@ func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap
6154 end := time.Now()
6255 durationMS := float64(end.Sub(start).Microseconds()) / 1000.0
6356
64- entry := accesslog.Entry{
57+ entry := out.AccessLogEntry{
6558 Time: end.UTC(),
6659 ClientIP: clientIP,
6760 Method: r.Method,
@@ -8,16 +8,9 @@ import (
8 8
9 "github.com/bnema/zerowrap"9 "github.com/bnema/zerowrap"
10 10
11- "github.com/bnema/gordon/internal/adapters/out/accesslog"11+ out "github.com/bnema/gordon/internal/boundaries/out"
12- "github.com/bnema/gordon/internal/domain"
13 )12 )
14 13
15-// AccessLogWriter is the interface the middleware needs from the output adapter.
16-// *accesslog.Writer satisfies this interface.
17-type AccessLogWriter interface {
18- Write(entry accesslog.Entry) error
19-}
20-
21 // AccessLogger is a middleware that writes one access-log entry per HTTP request14 // AccessLogger is a middleware that writes one access-log entry per HTTP request
22 // to the provided writer. It runs alongside (not instead of) RequestLogger.15 // to the provided writer. It runs alongside (not instead of) RequestLogger.
23 //16 //
@@ -26,7 +19,7 @@ type AccessLogWriter interface {
26 //19 //
27 // Write failures are reported as warnings through the application logger and20 // Write failures are reported as warnings through the application logger and
28 // never fail the HTTP response.21 // never fail the HTTP response.
29-func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap.Logger, trustedNets []*net.IPNet) func(http.Handler) http.Handler {22+func AccessLogger(writer out.AccessLogWriter, excludeHealthChecks bool, log zerowrap.Logger, trustedNets []*net.IPNet) func(http.Handler) http.Handler {
30 return func(next http.Handler) http.Handler {23 return func(next http.Handler) http.Handler {
31 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {24 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32 start := time.Now()25 start := time.Now()
@@ -50,7 +43,7 @@ func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap
50 // filter can never drift. Loopback check reuses localhostNets from43 // filter can never drift. Loopback check reuses localhostNets from
51 // cidr.go (same package) — no separate loopback definition needed.44 // cidr.go (same package) — no separate loopback definition needed.
52 if excludeHealthChecks {45 if excludeHealthChecks {
53- if strings.HasPrefix(r.UserAgent(), domain.HealthCheckUserAgentPrefix) ||46+ if strings.HasPrefix(r.UserAgent(), out.HealthCheckUserAgentPrefix) ||
54 IsTrustedProxy(clientIP, localhostNets) {47 IsTrustedProxy(clientIP, localhostNets) {
55 return48 return
56 }49 }
@@ -61,7 +54,7 @@ func AccessLogger(writer AccessLogWriter, excludeHealthChecks bool, log zerowrap
61 end := time.Now()54 end := time.Now()
62 durationMS := float64(end.Sub(start).Microseconds()) / 1000.055 durationMS := float64(end.Sub(start).Microseconds()) / 1000.0
63 56
64- entry := accesslog.Entry{57+ entry := out.AccessLogEntry{
65 Time: end.UTC(),58 Time: end.UTC(),
66 ClientIP: clientIP,59 ClientIP: clientIP,
67 Method: r.Method,60 Method: r.Method,
M internal/adapters/in/http/middleware/accesslog_test.go +3 -3
@@ -9,16 +9,16 @@ import (
99 "github.com/stretchr/testify/assert"
1010 "github.com/stretchr/testify/require"
1111
12- "github.com/bnema/gordon/internal/adapters/out/accesslog"
12+ out "github.com/bnema/gordon/internal/boundaries/out"
1313 )
1414
1515 // mockAccessLogWriter records written entries and optionally returns an error.
1616 type mockAccessLogWriter struct {
17- entries []accesslog.Entry
17+ entries []out.AccessLogEntry
1818 err error
1919 }
2020
21-func (m *mockAccessLogWriter) Write(entry accesslog.Entry) error {
21+func (m *mockAccessLogWriter) Write(entry out.AccessLogEntry) error {
2222 m.entries = append(m.entries, entry)
2323 return m.err
2424 }
@@ -9,16 +9,16 @@ import (
9 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 11
12- "github.com/bnema/gordon/internal/adapters/out/accesslog"12+ out "github.com/bnema/gordon/internal/boundaries/out"
13 )13 )
14 14
15 // mockAccessLogWriter records written entries and optionally returns an error.15 // mockAccessLogWriter records written entries and optionally returns an error.
16 type mockAccessLogWriter struct {16 type mockAccessLogWriter struct {
17- entries []accesslog.Entry17+ entries []out.AccessLogEntry
18 err error18 err error
19 }19 }
20 20
21-func (m *mockAccessLogWriter) Write(entry accesslog.Entry) error {21+func (m *mockAccessLogWriter) Write(entry out.AccessLogEntry) error {
22 m.entries = append(m.entries, entry)22 m.entries = append(m.entries, entry)
23 return m.err23 return m.err
24 }24 }
M internal/adapters/out/accesslog/formatter.go +5 -3
@@ -4,6 +4,8 @@ import (
44 "encoding/json"
55 "fmt"
66 "strings"
7+
8+ out "github.com/bnema/gordon/internal/boundaries/out"
79 )
810
911 // jsonEntry is the stable JSON schema for access log entries.
@@ -34,7 +36,7 @@ const jsonTimestamp = "2006-01-02T15:04:05.000Z07:00"
3436 // formatJSON serializes entry as a single-line JSON object.
3537 // Timestamps are RFC3339 with millisecond precision in UTC.
3638 // time.Time's default JSON marshaling is NOT used to ensure stable precision.
37-func formatJSON(e Entry) (string, error) {
39+func formatJSON(e out.AccessLogEntry) (string, error) {
3840 je := jsonEntry{
3941 Time: e.Time.UTC().Format(jsonTimestamp),
4042 ClientIP: e.ClientIP,
@@ -60,7 +62,7 @@ func formatJSON(e Entry) (string, error) {
6062 // formatCLF serializes entry in Common Log Format (NCSA).
6163 //
6264 // <ip> - - [timestamp] "METHOD target PROTO" status bytes
63-func formatCLF(e Entry) (string, error) {
65+func formatCLF(e out.AccessLogEntry) (string, error) {
6466 ts := e.Time.UTC().Format(clfTimestamp)
6567 target := buildRequestTarget(e.Path, e.Query)
6668 return fmt.Sprintf(`%s - - [%s] "%s %s %s" %d %d`,
@@ -77,7 +79,7 @@ func formatCLF(e Entry) (string, error) {
7779 // formatCombined serializes entry in Combined Log Format (CLF + referer + UA).
7880 //
7981 // <ip> - - [timestamp] "METHOD target PROTO" status bytes "referer" "ua"
80-func formatCombined(e Entry) (string, error) {
82+func formatCombined(e out.AccessLogEntry) (string, error) {
8183 ts := e.Time.UTC().Format(clfTimestamp)
8284 target := buildRequestTarget(e.Path, e.Query)
8385 referer := e.Referer
@@ -4,6 +4,8 @@ import (
4 "encoding/json"4 "encoding/json"
5 "fmt"5 "fmt"
6 "strings"6 "strings"
7+
8+ out "github.com/bnema/gordon/internal/boundaries/out"
7 )9 )
8 10
9 // jsonEntry is the stable JSON schema for access log entries.11 // jsonEntry is the stable JSON schema for access log entries.
@@ -34,7 +36,7 @@ const jsonTimestamp = "2006-01-02T15:04:05.000Z07:00"
34 // formatJSON serializes entry as a single-line JSON object.36 // formatJSON serializes entry as a single-line JSON object.
35 // Timestamps are RFC3339 with millisecond precision in UTC.37 // Timestamps are RFC3339 with millisecond precision in UTC.
36 // time.Time's default JSON marshaling is NOT used to ensure stable precision.38 // time.Time's default JSON marshaling is NOT used to ensure stable precision.
37-func formatJSON(e Entry) (string, error) {39+func formatJSON(e out.AccessLogEntry) (string, error) {
38 je := jsonEntry{40 je := jsonEntry{
39 Time: e.Time.UTC().Format(jsonTimestamp),41 Time: e.Time.UTC().Format(jsonTimestamp),
40 ClientIP: e.ClientIP,42 ClientIP: e.ClientIP,
@@ -60,7 +62,7 @@ func formatJSON(e Entry) (string, error) {
60 // formatCLF serializes entry in Common Log Format (NCSA).62 // formatCLF serializes entry in Common Log Format (NCSA).
61 //63 //
62 // <ip> - - [timestamp] "METHOD target PROTO" status bytes64 // <ip> - - [timestamp] "METHOD target PROTO" status bytes
63-func formatCLF(e Entry) (string, error) {65+func formatCLF(e out.AccessLogEntry) (string, error) {
64 ts := e.Time.UTC().Format(clfTimestamp)66 ts := e.Time.UTC().Format(clfTimestamp)
65 target := buildRequestTarget(e.Path, e.Query)67 target := buildRequestTarget(e.Path, e.Query)
66 return fmt.Sprintf(`%s - - [%s] "%s %s %s" %d %d`,68 return fmt.Sprintf(`%s - - [%s] "%s %s %s" %d %d`,
@@ -77,7 +79,7 @@ func formatCLF(e Entry) (string, error) {
77 // formatCombined serializes entry in Combined Log Format (CLF + referer + UA).79 // formatCombined serializes entry in Combined Log Format (CLF + referer + UA).
78 //80 //
79 // <ip> - - [timestamp] "METHOD target PROTO" status bytes "referer" "ua"81 // <ip> - - [timestamp] "METHOD target PROTO" status bytes "referer" "ua"
80-func formatCombined(e Entry) (string, error) {82+func formatCombined(e out.AccessLogEntry) (string, error) {
81 ts := e.Time.UTC().Format(clfTimestamp)83 ts := e.Time.UTC().Format(clfTimestamp)
82 target := buildRequestTarget(e.Path, e.Query)84 target := buildRequestTarget(e.Path, e.Query)
83 referer := e.Referer85 referer := e.Referer
M internal/adapters/out/accesslog/formatter_test.go +3 -1
@@ -8,11 +8,13 @@ import (
88
99 "github.com/stretchr/testify/assert"
1010 "github.com/stretchr/testify/require"
11+
12+ out "github.com/bnema/gordon/internal/boundaries/out"
1113 )
1214
1315 var testTime = time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC)
1416
15-var baseEntry = Entry{
17+var baseEntry = out.AccessLogEntry{
1618 Time: testTime,
1719 ClientIP: "135.181.213.219",
1820 Method: "GET",
@@ -8,11 +8,13 @@ import (
8 8
9 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11+
12+ out "github.com/bnema/gordon/internal/boundaries/out"
11 )13 )
12 14
13 var testTime = time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC)15 var testTime = time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC)
14 16
15-var baseEntry = Entry{17+var baseEntry = out.AccessLogEntry{
16 Time: testTime,18 Time: testTime,
17 ClientIP: "135.181.213.219",19 ClientIP: "135.181.213.219",
18 Method: "GET",20 Method: "GET",
M internal/adapters/out/accesslog/writer.go +6 -22
@@ -8,9 +8,10 @@ import (
88 "os"
99 "path/filepath"
1010 "sync"
11- "time"
1211
1312 "gopkg.in/natefinch/lumberjack.v2"
13+
14+ out "github.com/bnema/gordon/internal/boundaries/out"
1415 )
1516
1617 // Config holds configuration for the access log writer.
@@ -32,23 +33,6 @@ type Config struct {
3233 SyslogIdentifier string
3334 }
3435
35-// Entry holds the data for a single HTTP access log record.
36-type Entry struct {
37- Time time.Time
38- ClientIP string
39- Method string
40- Host string
41- Path string
42- Query string
43- Status int
44- BytesSent int
45- DurationMS float64
46- UserAgent string
47- Referer string
48- RequestID string
49- Proto string
50-}
51-
5236 // sink is the internal interface for output backends.
5337 type sink interface {
5438 WriteLine(line string) error
@@ -56,9 +40,9 @@ type sink interface {
5640 }
5741
5842 // Writer serializes access log entries and writes them to a configured sink.
59-// It is safe for concurrent use.
43+// It is safe for concurrent use. It implements out.AccessLogWriter.
6044 type Writer struct {
61- formatter func(Entry) (string, error)
45+ formatter func(out.AccessLogEntry) (string, error)
6246 sink sink
6347 mu sync.Mutex
6448 }
@@ -85,7 +69,7 @@ func New(cfg Config) (*Writer, error) {
8569 // Write serializes entry and writes it to the configured output. It is safe
8670 // for concurrent use. A write failure is returned as an error; callers should
8771 // log the error and continue — Write never panics.
88-func (w *Writer) Write(entry Entry) error {
72+func (w *Writer) Write(entry out.AccessLogEntry) error {
8973 line, err := w.formatter(entry)
9074 if err != nil {
9175 return fmt.Errorf("accesslog: format entry: %w", err)
@@ -108,7 +92,7 @@ func (w *Writer) Close() error {
10892 }
10993
11094 // newFormatter returns the format function for the given format name.
111-func newFormatter(format string) (func(Entry) (string, error), error) {
95+func newFormatter(format string) (func(out.AccessLogEntry) (string, error), error) {
11296 switch format {
11397 case "json":
11498 return formatJSON, nil
@@ -8,9 +8,10 @@ import (
8 "os"8 "os"
9 "path/filepath"9 "path/filepath"
10 "sync"10 "sync"
11- "time"
12 11
13 "gopkg.in/natefinch/lumberjack.v2"12 "gopkg.in/natefinch/lumberjack.v2"
13+
14+ out "github.com/bnema/gordon/internal/boundaries/out"
14 )15 )
15 16
16 // Config holds configuration for the access log writer.17 // Config holds configuration for the access log writer.
@@ -32,23 +33,6 @@ type Config struct {
32 SyslogIdentifier string33 SyslogIdentifier string
33 }34 }
34 35
35-// Entry holds the data for a single HTTP access log record.
36-type Entry struct {
37- Time time.Time
38- ClientIP string
39- Method string
40- Host string
41- Path string
42- Query string
43- Status int
44- BytesSent int
45- DurationMS float64
46- UserAgent string
47- Referer string
48- RequestID string
49- Proto string
50-}
51-
52 // sink is the internal interface for output backends.36 // sink is the internal interface for output backends.
53 type sink interface {37 type sink interface {
54 WriteLine(line string) error38 WriteLine(line string) error
@@ -56,9 +40,9 @@ type sink interface {
56 }40 }
57 41
58 // Writer serializes access log entries and writes them to a configured sink.42 // Writer serializes access log entries and writes them to a configured sink.
59-// It is safe for concurrent use.43+// It is safe for concurrent use. It implements out.AccessLogWriter.
60 type Writer struct {44 type Writer struct {
61- formatter func(Entry) (string, error)45+ formatter func(out.AccessLogEntry) (string, error)
62 sink sink46 sink sink
63 mu sync.Mutex47 mu sync.Mutex
64 }48 }
@@ -85,7 +69,7 @@ func New(cfg Config) (*Writer, error) {
85 // Write serializes entry and writes it to the configured output. It is safe69 // Write serializes entry and writes it to the configured output. It is safe
86 // for concurrent use. A write failure is returned as an error; callers should70 // for concurrent use. A write failure is returned as an error; callers should
87 // log the error and continue — Write never panics.71 // log the error and continue — Write never panics.
88-func (w *Writer) Write(entry Entry) error {72+func (w *Writer) Write(entry out.AccessLogEntry) error {
89 line, err := w.formatter(entry)73 line, err := w.formatter(entry)
90 if err != nil {74 if err != nil {
91 return fmt.Errorf("accesslog: format entry: %w", err)75 return fmt.Errorf("accesslog: format entry: %w", err)
@@ -108,7 +92,7 @@ func (w *Writer) Close() error {
108 }92 }
109 93
110 // newFormatter returns the format function for the given format name.94 // newFormatter returns the format function for the given format name.
111-func newFormatter(format string) (func(Entry) (string, error), error) {95+func newFormatter(format string) (func(out.AccessLogEntry) (string, error), error) {
112 switch format {96 switch format {
113 case "json":97 case "json":
114 return formatJSON, nil98 return formatJSON, nil
M internal/adapters/out/accesslog/writer_test.go +3 -1
@@ -13,9 +13,11 @@ import (
1313
1414 "github.com/stretchr/testify/assert"
1515 "github.com/stretchr/testify/require"
16+
17+ out "github.com/bnema/gordon/internal/boundaries/out"
1618 )
1719
18-var sampleEntry = Entry{
20+var sampleEntry = out.AccessLogEntry{
1921 Time: time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC),
2022 ClientIP: "10.0.0.1",
2123 Method: "POST",
@@ -13,9 +13,11 @@ import (
13 13
14 "github.com/stretchr/testify/assert"14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
16+
17+ out "github.com/bnema/gordon/internal/boundaries/out"
16 )18 )
17 19
18-var sampleEntry = Entry{20+var sampleEntry = out.AccessLogEntry{
19 Time: time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC),21 Time: time.Date(2026, 4, 3, 14, 20, 54, 167_000_000, time.UTC),
20 ClientIP: "10.0.0.1",22 ClientIP: "10.0.0.1",
21 Method: "POST",23 Method: "POST",
M internal/adapters/out/httpprober/prober.go +2 -2
@@ -8,7 +8,7 @@ import (
88 "net/http"
99 "time"
1010
11- "github.com/bnema/gordon/internal/domain"
11+ out "github.com/bnema/gordon/internal/boundaries/out"
1212 )
1313
1414 // DefaultTimeout is the default timeout for HTTP probes.
@@ -79,7 +79,7 @@ func (p *Prober) Probe(ctx context.Context, url string) (int, int64, error) {
7979 }
8080
8181 // Set a recognisable user agent so access-log filters can exclude probe traffic.
82- req.Header.Set("User-Agent", domain.HealthCheckUserAgentPrefix+"1.0")
82+ req.Header.Set("User-Agent", out.HealthCheckUserAgentPrefix+"1.0")
8383
8484 resp, err := p.client.Do(req)
8585 if err != nil {
@@ -8,7 +8,7 @@ import (
8 "net/http"8 "net/http"
9 "time"9 "time"
10 10
11- "github.com/bnema/gordon/internal/domain"11+ out "github.com/bnema/gordon/internal/boundaries/out"
12 )12 )
13 13
14 // DefaultTimeout is the default timeout for HTTP probes.14 // DefaultTimeout is the default timeout for HTTP probes.
@@ -79,7 +79,7 @@ func (p *Prober) Probe(ctx context.Context, url string) (int, int64, error) {
79 }79 }
80 80
81 // Set a recognisable user agent so access-log filters can exclude probe traffic.81 // Set a recognisable user agent so access-log filters can exclude probe traffic.
82- req.Header.Set("User-Agent", domain.HealthCheckUserAgentPrefix+"1.0")82+ req.Header.Set("User-Agent", out.HealthCheckUserAgentPrefix+"1.0")
83 83
84 resp, err := p.client.Do(req)84 resp, err := p.client.Do(req)
85 if err != nil {85 if err != nil {
M internal/app/run.go +2 -2
@@ -1660,7 +1660,7 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
16601660
16611661 // createHTTPHandlers creates HTTP handlers with middleware.
16621662 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1663-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter middleware.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {
1663+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter out.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {
16641664 // Parse trusted proxies once for all middleware chains.
16651665 // This ensures consistent IP extraction across logging, rate limiting, and auth.
16661666 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
@@ -2055,7 +2055,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
20552055 }
20562056 // Convert to interface only when non-nil to avoid the Go nil-interface pitfall
20572057 // where a typed nil pointer becomes a non-nil interface value.
2058- var accessWriter middleware.AccessLogWriter
2058+ var accessWriter out.AccessLogWriter
20592059 if accessWriterConcrete != nil {
20602060 accessWriter = accessWriterConcrete
20612061 }
@@ -1660,7 +1660,7 @@ func loopbackOnly(next http.Handler, log zerowrap.Logger) http.Handler {
1660 1660
1661 // createHTTPHandlers creates HTTP handlers with middleware.1661 // createHTTPHandlers creates HTTP handlers with middleware.
1662 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.1662 // Returns three handlers: registry, HTTP proxy (with CIDR + onboarding), and HTTPS proxy.
1663-func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter middleware.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {1663+func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger, accessWriter out.AccessLogWriter) (http.Handler, http.Handler, http.Handler) {
1664 // Parse trusted proxies once for all middleware chains.1664 // Parse trusted proxies once for all middleware chains.
1665 // This ensures consistent IP extraction across logging, rate limiting, and auth.1665 // This ensures consistent IP extraction across logging, rate limiting, and auth.
1666 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)1666 trustedNets := middleware.ParseTrustedProxies(cfg.API.RateLimit.TrustedProxies)
@@ -2055,7 +2055,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
2055 }2055 }
2056 // Convert to interface only when non-nil to avoid the Go nil-interface pitfall2056 // Convert to interface only when non-nil to avoid the Go nil-interface pitfall
2057 // where a typed nil pointer becomes a non-nil interface value.2057 // where a typed nil pointer becomes a non-nil interface value.
2058- var accessWriter middleware.AccessLogWriter2058+ var accessWriter out.AccessLogWriter
2059 if accessWriterConcrete != nil {2059 if accessWriterConcrete != nil {
2060 accessWriter = accessWriterConcrete2060 accessWriter = accessWriterConcrete
2061 }2061 }
M internal/app/run_http_handlers_test.go +7 -7
@@ -15,29 +15,29 @@ import (
1515 "github.com/stretchr/testify/require"
1616
1717 adminhttp "github.com/bnema/gordon/internal/adapters/in/http/admin"
18- "github.com/bnema/gordon/internal/adapters/out/accesslog"
1918 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
19+ out "github.com/bnema/gordon/internal/boundaries/out"
2020 )
2121
2222 // testAccessLogWriter is a thread-safe mock AccessLogWriter for tests.
2323 type testAccessLogWriter struct {
2424 mu sync.Mutex
25- entries []accesslog.Entry
25+ entries []out.AccessLogEntry
2626 }
2727
28-func (w *testAccessLogWriter) Write(entry accesslog.Entry) error {
28+func (w *testAccessLogWriter) Write(entry out.AccessLogEntry) error {
2929 w.mu.Lock()
3030 defer w.mu.Unlock()
3131 w.entries = append(w.entries, entry)
3232 return nil
3333 }
3434
35-func (w *testAccessLogWriter) snapshot() []accesslog.Entry {
35+func (w *testAccessLogWriter) snapshot() []out.AccessLogEntry {
3636 w.mu.Lock()
3737 defer w.mu.Unlock()
38- out := make([]accesslog.Entry, len(w.entries))
39- copy(out, w.entries)
40- return out
38+ result := make([]out.AccessLogEntry, len(w.entries))
39+ copy(result, w.entries)
40+ return result
4141 }
4242
4343 func TestCreateAuthService_Disabled_ReturnsNilServices(t *testing.T) {
@@ -15,29 +15,29 @@ import (
15 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
16 16
17 adminhttp "github.com/bnema/gordon/internal/adapters/in/http/admin"17 adminhttp "github.com/bnema/gordon/internal/adapters/in/http/admin"
18- "github.com/bnema/gordon/internal/adapters/out/accesslog"
19 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"18 pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
19+ out "github.com/bnema/gordon/internal/boundaries/out"
20 )20 )
21 21
22 // testAccessLogWriter is a thread-safe mock AccessLogWriter for tests.22 // testAccessLogWriter is a thread-safe mock AccessLogWriter for tests.
23 type testAccessLogWriter struct {23 type testAccessLogWriter struct {
24 mu sync.Mutex24 mu sync.Mutex
25- entries []accesslog.Entry25+ entries []out.AccessLogEntry
26 }26 }
27 27
28-func (w *testAccessLogWriter) Write(entry accesslog.Entry) error {28+func (w *testAccessLogWriter) Write(entry out.AccessLogEntry) error {
29 w.mu.Lock()29 w.mu.Lock()
30 defer w.mu.Unlock()30 defer w.mu.Unlock()
31 w.entries = append(w.entries, entry)31 w.entries = append(w.entries, entry)
32 return nil32 return nil
33 }33 }
34 34
35-func (w *testAccessLogWriter) snapshot() []accesslog.Entry {35+func (w *testAccessLogWriter) snapshot() []out.AccessLogEntry {
36 w.mu.Lock()36 w.mu.Lock()
37 defer w.mu.Unlock()37 defer w.mu.Unlock()
38- out := make([]accesslog.Entry, len(w.entries))38+ result := make([]out.AccessLogEntry, len(w.entries))
39- copy(out, w.entries)39+ copy(result, w.entries)
40- return out40+ return result
41 }41 }
42 42
43 func TestCreateAuthService_Disabled_ReturnsNilServices(t *testing.T) {43 func TestCreateAuthService_Disabled_ReturnsNilServices(t *testing.T) {
A internal/boundaries/out/accesslog.go +34 -0
@@ -0,0 +1,34 @@
1+package out
2+
3+import "time"
4+
5+// AccessLogEntry represents a single HTTP access log record.
6+// It is the neutral data contract between the access log middleware (adapters/in)
7+// and the access log writer (adapters/out). Keeping it in the boundaries layer
8+// prevents direct adapter-to-adapter coupling.
9+type AccessLogEntry struct {
10+ Time time.Time
11+ ClientIP string
12+ Method string
13+ Host string
14+ Path string
15+ Query string
16+ Status int
17+ BytesSent int
18+ DurationMS float64
19+ UserAgent string
20+ Referer string
21+ RequestID string
22+ Proto string
23+}
24+
25+// AccessLogWriter writes HTTP access log entries to a configured sink.
26+// *accesslog.Writer in internal/adapters/out/accesslog implements this interface.
27+type AccessLogWriter interface {
28+ Write(entry AccessLogEntry) error
29+}
30+
31+// HealthCheckUserAgentPrefix is the User-Agent prefix set by Gordon's internal
32+// health-check prober. Access log middleware uses this to filter out health-check
33+// noise; the prober uses it to construct the full User-Agent string.
34+const HealthCheckUserAgentPrefix = "Gordon-HealthCheck/"
@@ -0,0 +1,34 @@
1+package out
2+
3+import "time"
4+
5+// AccessLogEntry represents a single HTTP access log record.
6+// It is the neutral data contract between the access log middleware (adapters/in)
7+// and the access log writer (adapters/out). Keeping it in the boundaries layer
8+// prevents direct adapter-to-adapter coupling.
9+type AccessLogEntry struct {
10+ Time time.Time
11+ ClientIP string
12+ Method string
13+ Host string
14+ Path string
15+ Query string
16+ Status int
17+ BytesSent int
18+ DurationMS float64
19+ UserAgent string
20+ Referer string
21+ RequestID string
22+ Proto string
23+}
24+
25+// AccessLogWriter writes HTTP access log entries to a configured sink.
26+// *accesslog.Writer in internal/adapters/out/accesslog implements this interface.
27+type AccessLogWriter interface {
28+ Write(entry AccessLogEntry) error
29+}
30+
31+// HealthCheckUserAgentPrefix is the User-Agent prefix set by Gordon's internal
32+// health-check prober. Access log middleware uses this to filter out health-check
33+// noise; the prober uses it to construct the full User-Agent string.
34+const HealthCheckUserAgentPrefix = "Gordon-HealthCheck/"
M internal/domain/labels.go +0 -4
@@ -1,9 +1,5 @@
11 package domain
22
3-// HealthCheckUserAgentPrefix is the User-Agent prefix set by Gordon's internal
4-// health-check prober. Shared so access-log filtering and the prober stay in sync.
5-const HealthCheckUserAgentPrefix = "Gordon-HealthCheck/"
6-
73 // Label keys used by Gordon for container and image metadata.
84 const (
95 // Container labels
@@ -1,9 +1,5 @@
1 package domain1 package domain
2 2
3-// HealthCheckUserAgentPrefix is the User-Agent prefix set by Gordon's internal
4-// health-check prober. Shared so access-log filtering and the prober stay in sync.
5-const HealthCheckUserAgentPrefix = "Gordon-HealthCheck/"
6-
7 // Label keys used by Gordon for container and image metadata.3 // Label keys used by Gordon for container and image metadata.
8 const (4 const (
9 // Container labels5 // Container labels

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close