Line data Source code
1 : // Package middleware はリクエストのロギングなどのミドルウェアを提供します。
2 : package middleware
3 :
4 : import (
5 : "bytes"
6 : "encoding/json"
7 : "io"
8 : "log/slog"
9 : "strings"
10 : "time"
11 :
12 : "github.com/gin-gonic/gin"
13 :
14 : "resume/internal/shared/requestid"
15 : "resume/internal/shared/util"
16 : )
17 :
18 : // RequestLoggerOptions はリクエストロガーミドルウェアの動作を制御します。
19 : type RequestLoggerOptions struct {
20 : // true を返したリクエストはロギングしない(/health など)
21 : Skipper func(*gin.Context) bool
22 : // 追加で残したいヘッダ(値は req.headers にマップ化)
23 : IncludeHeaders []string
24 : // 追加:任意の Masker。nil の場合は util.GlobalMasker() を使う。
25 : Masker util.Masker
26 : }
27 :
28 : //const ctxKeyRawBody = "request.rawBody" // 後段で見たいとき用に保存
29 :
30 : // RequestLogger は Gin のリクエスト/レスポンス情報を構造化ログで出力するミドルウェアを返します。
31 2 : func RequestLogger(l *slog.Logger, opt *RequestLoggerOptions) gin.HandlerFunc {
32 2 : // nil セーフな初期化
33 2 : var (
34 2 : skipper func(*gin.Context) bool
35 2 : includeHeader = map[string]struct{}{}
36 2 : masker util.Masker
37 2 : )
38 3 : if opt != nil && opt.Skipper != nil {
39 1 : skipper = opt.Skipper
40 1 : }
41 4 : if opt != nil && len(opt.IncludeHeaders) > 0 {
42 4 : for _, h := range opt.IncludeHeaders {
43 2 : includeHeader[strings.ToLower(h)] = struct{}{}
44 2 : }
45 : }
46 : // マスカーは指定がなければグローバルを使用
47 2 : if opt != nil && opt.Masker != nil {
48 0 : masker = opt.Masker
49 2 : } else {
50 2 : masker = util.GlobalMasker()
51 2 : }
52 :
53 3 : normalize := func(h string) string { return strings.ToLower(h) }
54 :
55 4 : return func(c *gin.Context) {
56 2 : // スキップ条件
57 3 : if skipper != nil && skipper(c) {
58 1 : c.Next()
59 1 : return
60 1 : }
61 :
62 2 : start := time.Now()
63 2 :
64 2 : // Request 基本情報
65 2 : reqID := requestid.Ensure(c)
66 2 : method := c.Request.Method
67 2 : path := c.FullPath()
68 2 : if path == "" { // ルート未マッチ時は生パス
69 0 : path = c.Request.URL.Path
70 0 : }
71 2 : host := c.Request.Host
72 2 : proto := c.Request.Proto
73 2 : ip := c.ClientIP()
74 2 : ua := c.Request.UserAgent()
75 2 :
76 2 : // ---- クエリ(マスク対象)----
77 2 : qmap := make(map[string]any, len(c.Request.URL.Query()))
78 2 : for k, vs := range c.Request.URL.Query() {
79 0 : if len(vs) > 0 {
80 0 : qmap[k] = vs[0]
81 0 : }
82 : }
83 2 : maskedQuery := masker.MaskMapShallow(qmap)
84 2 :
85 2 : // ---- ヘッダ(IncludeHeaders のみ・マスク対象)----
86 2 : hmap := map[string]any{}
87 3 : for k, v := range c.Request.Header {
88 2 : if _, ok := includeHeader[normalize(k)]; ok && len(v) > 0 {
89 1 : hmap[k] = masker.MaskByKey(k, v[0])
90 1 : }
91 : }
92 :
93 : // ---- ボディ(JSON優先で判定→再帰マスク。非JSONはそのまま)----
94 2 : var bodyLogged any
95 2 : if c.Request.Body != nil && c.Request.ContentLength != 0 {
96 0 : if b, err := io.ReadAll(c.Request.Body); err == nil {
97 0 : c.Request.Body = io.NopCloser(bytes.NewBuffer(b)) // 差し戻し
98 0 :
99 0 : var anyJSON any
100 0 : if len(b) > 0 && json.Unmarshal(b, &anyJSON) == nil {
101 0 : // ✅ JSON として読めたら再帰マスク
102 0 : masked := masker.MaskAnyRecursive("", anyJSON)
103 0 : if enc, err := json.Marshal(masked); err == nil {
104 0 : bodyLogged = string(enc)
105 0 : } else {
106 0 : bodyLogged = string(b) // フォールバック
107 0 : }
108 0 : } else {
109 0 : // 非JSONはそのまま(必要ならここで独自マスク)
110 0 : bodyLogged = string(b)
111 0 : }
112 : }
113 : }
114 :
115 : // 実処理
116 2 : c.Next()
117 2 :
118 2 : // Response 側
119 2 : status := c.Writer.Status()
120 2 : size := c.Writer.Size() // -1 の場合あり
121 2 : latMs := time.Since(start).Milliseconds()
122 2 :
123 2 : // Gin のエラー(c.Error されたもの)
124 2 : var errMsg string
125 2 : if len(c.Errors) > 0 {
126 0 : errMsg = c.Errors.String()
127 0 : }
128 :
129 2 : attrs := []any{
130 2 : "request_id", reqID,
131 2 : slog.Group("req",
132 2 : "method", method,
133 2 : "path", path,
134 2 : "host", host,
135 2 : "proto", proto,
136 2 : "ip", ip,
137 2 : "user_agent", ua,
138 2 : "query", maskedQuery, // ← マスク済み
139 2 : "headers", hmap, // ← マスク済み
140 2 : "content_length", c.Request.ContentLength,
141 2 : "body", bodyLogged, // JSON はマスク済み文字列
142 2 : ),
143 2 : slog.Group("res",
144 2 : "status", status,
145 2 : "size", size,
146 2 : ),
147 2 : "duration_ms", latMs,
148 2 : }
149 2 :
150 2 : if errMsg != "" {
151 0 : attrs = append(attrs, "error", errMsg)
152 0 : l.Error("http.request", attrs...)
153 0 : return
154 0 : }
155 2 : l.Info("http.request", attrs...)
156 : }
157 : }
|