LCOV - code coverage report
Current view: top level - adapter/http/middleware - request_logger.go Coverage Total Hit
Test: coverage.lcov Lines: 74.8 % 111 83
Test Date: 2026-04-14 06:42:22 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.3.1-1