Repository navigation
Expand file tree
/
Copy pathlevels_http.go
More file actions
169 lines (134 loc) · 4.76 KB
/
Copy pathlevels_http.go
File metadata and controls
169 lines (134 loc) · 4.76 KB
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package logging
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
)
// maxLevelsRequestSize limits the PUT request body of LevelsHandler.
const maxLevelsRequestSize = 4 << 10
// LevelsState is the response of LevelsHandler: the global level and the effective level of every known
// named logger, which is a logger created by name or configured in Config.CustomLevels.
type LevelsState struct {
Level string `json:"level"`
Loggers map[string]string `json:"loggers"`
}
// LevelsRequest is the PUT request body of LevelsHandler.
//
// {"level": "debug"} set the global level
// {"logger": "db", "level": "debug"} set the level of the named loggers
// {"logger": "db", "reset": true} restore the configured level of the named loggers
type LevelsRequest struct {
Logger string `json:"logger,omitempty"`
Level string `json:"level,omitempty"`
Reset bool `json:"reset,omitempty"`
}
// levels returns the global level and effective levels of the known named loggers.
func (st *state) levels() (Level, map[string]Level) {
names := make(map[string]struct{}, len(st.config.CustomLevels))
for name := range st.config.CustomLevels {
names[name] = struct{}{}
}
st.mu.Lock()
for name := range st.named {
names[name] = struct{}{}
}
st.mu.Unlock()
global := st.global.load()
res := make(map[string]Level, len(names))
for name := range names {
if level := st.namedVar(name).load(); level != levelUnset {
res[name] = level
} else {
res[name] = global
}
}
return global, res
}
// LevelsHandler returns http.Handler to view and change logging levels of the active backend at run time.
// It follows Init: every request is served using the backend active at that moment.
//
// GET returns LevelsState as JSON
// PUT takes LevelsRequest as JSON, applies it and returns the new LevelsState
//
// Levels are named the way ParseLevel takes them. Any other method gets 405, malformed or invalid request gets 400.
//
// SECURITY: the handler is disabled by default and answers 404 until Config.LevelsEndpoint is set
// (levelsEndpoint: true, WithLevelsEndpoint). It has no authentication: anyone able to reach an enabled one
// may make the service log every detail or hide its errors. Mount it on an administrative listener
// closed from the outside.
func LevelsHandler() http.Handler {
return levelsHandler(activeBackend)
}
// LevelsHandler returns http.Handler to view and change logging levels of the backend, see LevelsHandler.
func (backend *Backend) LevelsHandler() http.Handler {
return levelsHandler(func() *Backend { return backend })
}
type levelsHandlerFunc func() *Backend
func levelsHandler(get levelsHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backend := get()
if !backend.state().config.LevelsEndpoint {
http.NotFound(w, r) // do not tell the endpoint exists
return
}
switch r.Method {
case http.MethodGet:
case http.MethodPut:
if err := applyLevelsRequest(backend, w, r); err != nil {
writeLevelsError(w, http.StatusBadRequest, err)
return
}
default:
w.Header().Set("Allow", "GET, PUT")
writeLevelsError(w, http.StatusMethodNotAllowed, fmt.Errorf("method %s not allowed", r.Method))
return
}
writeLevelsState(w, backend)
})
}
func applyLevelsRequest(backend *Backend, w http.ResponseWriter, r *http.Request) error {
var req LevelsRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxLevelsRequestSize))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
if req.Reset {
if req.Logger == "" || req.Level != "" {
return errors.New("reset takes a logger name and no level")
}
backend.ResetNamedLevel(req.Logger)
return nil
}
level, err := ParseLevel(req.Level)
if err != nil {
return err
}
if req.Logger == "" {
return backend.SetLevel(level)
}
return backend.SetNamedLevel(req.Logger, level)
}
func writeLevelsState(w http.ResponseWriter, backend *Backend) {
global, named := backend.state().levels()
res := LevelsState{Level: global.String(), Loggers: make(map[string]string, len(named))}
names := make([]string, 0, len(named))
for name := range named {
names = append(names, name)
}
sort.Strings(names) // encoding/json sorts map keys anyway, keep the order explicit for readers
for _, name := range names {
res.Loggers[name] = named[name].String()
}
writeLevelsJSON(w, http.StatusOK, res)
}
func writeLevelsError(w http.ResponseWriter, status int, err error) {
writeLevelsJSON(w, status, map[string]string{"error": err.Error()})
}
func writeLevelsJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}