aboutsummaryrefslogtreecommitdiffstats
path: root/services/context/access_log.go
blob: caade113a7d6d4395635e9825eae545a90807724 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package context

import (
	"bytes"
	"net"
	"net/http"
	"strings"
	"text/template"
	"time"

	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/web/middleware"
)

type accessLoggerTmplData struct {
	Identity       *string
	Start          *time.Time
	ResponseWriter struct {
		Status, Size int
	}
	Ctx       map[string]any
	RequestID *string
}

const keyOfRequestIDInTemplate = ".RequestID"

// According to:
// TraceId: A valid trace identifier is a 16-byte array with at least one non-zero byte
// MD5 output is 16 or 32 bytes: md5-bytes is 16, md5-hex is 32
// SHA1: similar, SHA1-bytes is 20, SHA1-hex is 40.
// UUID is 128-bit, 32 hex chars, 36 ASCII chars with 4 dashes
// So, we accept a Request ID with a maximum character length of 40
const maxRequestIDByteLength = 40

func parseRequestIDFromRequestHeader(req *http.Request) string {
	requestID := "-"
	for _, key := range setting.Log.RequestIDHeaders {
		if req.Header.Get(key) != "" {
			requestID = req.Header.Get(key)
			break
		}
	}
	if len(requestID) > maxRequestIDByteLength {
		requestID = requestID[:maxRequestIDByteLength] + "..."
	}
	return requestID
}

type accessLogRecorder struct {
	logger        log.BaseLogger
	logTemplate   *template.Template
	needRequestID bool
}

func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
	var requestID string
	if lr.needRequestID {
		requestID = parseRequestIDFromRequestHeader(req)
	}

	reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
	if err != nil {
		reqHost = req.RemoteAddr
	}

	identity := "-"
	data := middleware.GetContextData(req.Context())
	if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
		identity = signedUser.Name
	}
	buf := bytes.NewBuffer([]byte{})
	tmplData := accessLoggerTmplData{
		Identity: &identity,
		Start:    &start,
		Ctx: map[string]any{
			"RemoteAddr": req.RemoteAddr,
			"RemoteHost": reqHost,
			"Req":        req,
		},
		RequestID: &requestID,
	}
	tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
	tmplData.ResponseWriter.Size = respWriter.WrittenSize()
	err = lr.logTemplate.Execute(buf, tmplData)
	if err != nil {
		log.Error("Could not execute access logger template: %v", err.Error())
	}

	lr.logger.Log(1, &log.Event{Level: log.INFO}, "%s", buf.String())
}

func newAccessLogRecorder() *accessLogRecorder {
	return &accessLogRecorder{
		logger:        log.GetLogger("access"),
		logTemplate:   template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)),
		needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate),
	}
}

// AccessLogger returns a middleware to log access logger
func AccessLogger() func(http.Handler) http.Handler {
	recorder := newAccessLogRecorder()
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			start := time.Now()
			next.ServeHTTP(w, req)
			recorder.record(start, w.(ResponseWriter), req)
		})
	}
}