Skip to content

Commit 950449a

Browse files
authored
feat: access log config (#1695)
1 parent 086e1bf commit 950449a

File tree

6 files changed

+138
-21
lines changed

6 files changed

+138
-21
lines changed

internal/cmn/config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type Server struct {
120120
BasePath string // URL path for reverse proxy subpath hosting
121121
APIBasePath string
122122
Headless bool
123+
AccessLog AccessLogMode // "all" (default), "non-public", or "none"
123124
LatestStatusToday bool
124125
TLS *TLSConfig
125126
Auth Auth
@@ -165,6 +166,15 @@ const (
165166
AuthModeBuiltin AuthMode = "builtin"
166167
)
167168

169+
// AccessLogMode represents the HTTP access log mode.
170+
type AccessLogMode string
171+
172+
const (
173+
AccessLogAll AccessLogMode = "all"
174+
AccessLogNonPublic AccessLogMode = "non-public"
175+
AccessLogNone AccessLogMode = "none"
176+
)
177+
168178
// MetricsAccess represents the access mode for the metrics endpoint.
169179
type MetricsAccess string
170180

internal/cmn/config/definition.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ type Definition struct {
1313
TLS *TLSDef `mapstructure:"tls"`
1414

1515
// Core settings
16-
Debug bool `mapstructure:"debug"`
17-
DefaultShell string `mapstructure:"default_shell"`
18-
LogFormat string `mapstructure:"log_format"` // "json" or "text"
19-
TZ string `mapstructure:"tz"`
16+
Debug bool `mapstructure:"debug"`
17+
DefaultShell string `mapstructure:"default_shell"`
18+
LogFormat string `mapstructure:"log_format"` // "json" or "text"
19+
AccessLog *string `mapstructure:"access_log_mode"` // "all" (default), "non-public", or "none"
20+
TZ string `mapstructure:"tz"`
2021

2122
// Authentication
2223
Auth *AuthDef `mapstructure:"auth"`

internal/cmn/config/loader.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,16 @@ func (l *ConfigLoader) loadServerFlags(cfg *Config, def Definition) {
389389
if def.Headless != nil {
390390
cfg.Server.Headless = *def.Headless
391391
}
392+
cfg.Server.AccessLog = AccessLogAll
393+
if def.AccessLog != nil {
394+
switch AccessLogMode(*def.AccessLog) {
395+
case AccessLogAll, AccessLogNonPublic, AccessLogNone:
396+
cfg.Server.AccessLog = AccessLogMode(*def.AccessLog)
397+
default:
398+
l.warnings = append(l.warnings, fmt.Sprintf(
399+
"Invalid access_log_mode value: %q, defaulting to 'all'", *def.AccessLog))
400+
}
401+
}
392402
if def.LatestStatusToday != nil {
393403
cfg.Server.LatestStatusToday = *def.LatestStatusToday
394404
}
@@ -1114,6 +1124,7 @@ func (l *ConfigLoader) setViperDefaultValues(paths Paths) {
11141124
l.v.SetDefault("metrics", "private")
11151125
l.v.SetDefault("cache", "normal")
11161126
l.v.SetDefault("log_format", "text")
1127+
l.v.SetDefault("access_log_mode", "all")
11171128

11181129
// Coordinator
11191130
l.v.SetDefault("coordinator.host", "127.0.0.1")
@@ -1167,6 +1178,7 @@ type envBinding struct {
11671178
var envBindings = []envBinding{
11681179
// Server
11691180
{key: "log_format", env: "LOG_FORMAT"},
1181+
{key: "access_log_mode", env: "ACCESS_LOG_MODE"},
11701182
{key: "base_path", env: "BASE_PATH"},
11711183
{key: "api_base_url", env: "API_BASE_URL"},
11721184
{key: "tz", env: "TZ"},

internal/cmn/config/loader_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func TestLoad_Env(t *testing.T) {
9393
"DAGU_UI_DAGS_SORT_ORDER": "desc",
9494

9595
"DAGU_TERMINAL_ENABLED": "true",
96+
"DAGU_ACCESS_LOG_MODE": "none",
9697

9798
"DAGU_AUDIT_ENABLED": "false",
9899

@@ -138,6 +139,7 @@ func TestLoad_Env(t *testing.T) {
138139
BasePath: "/test/base",
139140
APIBasePath: "/test/api",
140141
Headless: true,
142+
AccessLog: AccessLogNone,
141143
Auth: Auth{
142144
Mode: AuthModeBasic, // Explicit basic mode from env
143145
Basic: AuthBasic{Username: "testuser", Password: "testpass"},
@@ -375,6 +377,7 @@ scheduler:
375377
BasePath: "/dagu",
376378
APIBasePath: "/api/v1",
377379
Headless: true,
380+
AccessLog: AccessLogAll,
378381
LatestStatusToday: true,
379382
Auth: Auth{
380383
Mode: AuthModeBasic, // Explicit basic mode from YAML
@@ -990,6 +993,53 @@ metrics: "invalid_value"
990993
})
991994
}
992995

996+
func TestLoad_AccessLogMode(t *testing.T) {
997+
t.Run("AccessLogAll", func(t *testing.T) {
998+
cfg := loadFromYAML(t, `
999+
access_log_mode: "all"
1000+
`)
1001+
assert.Equal(t, AccessLogAll, cfg.Server.AccessLog)
1002+
})
1003+
1004+
t.Run("AccessLogNonPublic", func(t *testing.T) {
1005+
cfg := loadFromYAML(t, `
1006+
access_log_mode: "non-public"
1007+
`)
1008+
assert.Equal(t, AccessLogNonPublic, cfg.Server.AccessLog)
1009+
})
1010+
1011+
t.Run("AccessLogNone", func(t *testing.T) {
1012+
cfg := loadFromYAML(t, `
1013+
access_log_mode: "none"
1014+
`)
1015+
assert.Equal(t, AccessLogNone, cfg.Server.AccessLog)
1016+
})
1017+
1018+
t.Run("AccessLogDefault", func(t *testing.T) {
1019+
cfg := loadFromYAML(t, "# empty")
1020+
assert.Equal(t, AccessLogAll, cfg.Server.AccessLog)
1021+
})
1022+
1023+
t.Run("AccessLogFromEnv", func(t *testing.T) {
1024+
cfg := loadWithEnv(t, "# empty", map[string]string{
1025+
"DAGU_ACCESS_LOG_MODE": "non-public",
1026+
})
1027+
assert.Equal(t, AccessLogNonPublic, cfg.Server.AccessLog)
1028+
})
1029+
1030+
t.Run("AccessLogInvalid", func(t *testing.T) {
1031+
cfg := loadFromYAML(t, `
1032+
auth:
1033+
mode: none
1034+
access_log_mode: "invalid"
1035+
`)
1036+
assert.Equal(t, AccessLogAll, cfg.Server.AccessLog)
1037+
require.Len(t, cfg.Warnings, 1)
1038+
assert.Contains(t, cfg.Warnings[0], "Invalid access_log_mode value")
1039+
assert.Contains(t, cfg.Warnings[0], "invalid")
1040+
})
1041+
}
1042+
9931043
func TestLoad_CacheConfig(t *testing.T) {
9941044
t.Run("DefaultCacheMode", func(t *testing.T) {
9951045
cfg := loadFromYAML(t, ``)

internal/cmn/schema/config.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
"type": "boolean",
3838
"description": "Enable debug mode with verbose logging."
3939
},
40+
"access_log_mode": {
41+
"type": "string",
42+
"enum": ["all", "non-public", "none"],
43+
"description": "HTTP access log mode. 'all' logs every request, 'non-public' skips public endpoints (/health, /auth/login, /auth/setup), 'none' disables access logging entirely. Default: 'all'.",
44+
"default": "all"
45+
},
4046
"default_shell": {
4147
"type": "string",
4248
"description": "Default shell for executing commands (e.g., '/bin/bash')."

internal/service/frontend/server.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import (
5353
"github.com/dagu-org/dagu/internal/service/audit"
5454
authservice "github.com/dagu-org/dagu/internal/service/auth"
5555
"github.com/dagu-org/dagu/internal/service/coordinator"
56+
"github.com/dagu-org/dagu/internal/service/frontend/api/pathutil"
5657
apiv1 "github.com/dagu-org/dagu/internal/service/frontend/api/v1"
5758
"github.com/dagu-org/dagu/internal/service/frontend/auth"
5859
"github.com/dagu-org/dagu/internal/service/frontend/metrics"
@@ -633,28 +634,65 @@ func redactTokenFromRequest(r *http.Request) *http.Request {
633634
return redacted
634635
}
635636

637+
// buildPublicPaths returns the set of public endpoint paths that should be
638+
// excluded from access logging in "non-public" mode.
639+
func buildPublicPaths(basePath string, metrics config.MetricsAccess) map[string]struct{} {
640+
paths := []string{
641+
pathutil.BuildPublicEndpointPath(basePath, "api/v1/health"),
642+
pathutil.BuildPublicEndpointPath(basePath, "api/v1/auth/login"),
643+
pathutil.BuildPublicEndpointPath(basePath, "api/v1/auth/setup"),
644+
}
645+
if metrics == config.MetricsAccessPublic {
646+
paths = append(paths, pathutil.BuildPublicEndpointPath(basePath, "api/v1/metrics"))
647+
}
648+
set := make(map[string]struct{}, len(paths))
649+
for _, p := range paths {
650+
set[p] = struct{}{}
651+
}
652+
return set
653+
}
654+
655+
// skipPathsMiddleware wraps a middleware to skip it for requests matching any of the given paths.
656+
func skipPathsMiddleware(mw func(http.Handler) http.Handler, skip map[string]struct{}) func(http.Handler) http.Handler {
657+
return func(next http.Handler) http.Handler {
658+
wrapped := mw(next)
659+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
660+
if _, ok := skip[r.URL.Path]; ok {
661+
next.ServeHTTP(w, r)
662+
return
663+
}
664+
wrapped.ServeHTTP(w, r)
665+
})
666+
}
667+
}
668+
636669
// Serve starts the HTTP server and configures routes.
637670
func (srv *Server) Serve(ctx context.Context) error {
638-
logLevel := slog.LevelInfo
639-
if srv.config.Core.Debug {
640-
logLevel = slog.LevelDebug
641-
}
642-
643-
requestLogger := httplog.NewLogger("http", httplog.Options{
644-
LogLevel: logLevel,
645-
JSON: srv.config.Core.LogFormat == "json",
646-
Concise: true,
647-
RequestHeaders: srv.config.Core.Debug,
648-
MessageFieldName: "msg",
649-
ResponseHeaders: false,
650-
QuietDownRoutes: []string{"/api/v1/events"},
651-
QuietDownPeriod: 10 * time.Second,
652-
})
653-
654671
r := chi.NewMux()
655672
r.Use(middleware.RealIP)
656673
r.Use(middleware.Compress(5))
657-
r.Use(sanitizedRequestLogger(requestLogger))
674+
if srv.config.Server.AccessLog != config.AccessLogNone {
675+
logLevel := slog.LevelInfo
676+
if srv.config.Core.Debug {
677+
logLevel = slog.LevelDebug
678+
}
679+
requestLogger := httplog.NewLogger("http", httplog.Options{
680+
LogLevel: logLevel,
681+
JSON: srv.config.Core.LogFormat == "json",
682+
Concise: true,
683+
RequestHeaders: srv.config.Core.Debug,
684+
MessageFieldName: "msg",
685+
ResponseHeaders: false,
686+
QuietDownRoutes: []string{"/api/v1/events"},
687+
QuietDownPeriod: 10 * time.Second,
688+
})
689+
logMiddleware := sanitizedRequestLogger(requestLogger)
690+
if srv.config.Server.AccessLog == config.AccessLogNonPublic {
691+
skipPaths := buildPublicPaths(srv.config.Server.BasePath, srv.config.Server.Metrics)
692+
logMiddleware = skipPathsMiddleware(logMiddleware, skipPaths)
693+
}
694+
r.Use(logMiddleware)
695+
}
658696
r.Use(middleware.Recoverer)
659697
r.Use(cors.Handler(cors.Options{
660698
AllowedOrigins: []string{"*"},

0 commit comments

Comments
 (0)