github.com/grafana/synthetic-monitoring-agent@v0.64.2/cmd/synthetic-monitoring-agent/http.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "net/http/pprof" 8 "os" 9 "strconv" 10 "strings" 11 "sync/atomic" 12 "syscall" 13 14 "github.com/felixge/httpsnoop" 15 "github.com/prometheus/client_golang/prometheus" 16 "github.com/prometheus/client_golang/prometheus/promhttp" 17 "github.com/rs/zerolog" 18 ) 19 20 type Mux struct { 21 logger zerolog.Logger 22 router *http.ServeMux 23 metrics metrics 24 } 25 26 type metrics struct { 27 inFlightRequests prometheus.Gauge 28 requestDurationVec *prometheus.SummaryVec 29 requestWrittenBytesVec *prometheus.SummaryVec 30 } 31 32 type MuxOpts struct { 33 Logger zerolog.Logger 34 PromRegisterer interface { 35 prometheus.Registerer 36 prometheus.Gatherer 37 } 38 isReady *readynessHandler 39 changeLogLevelEnabled bool 40 disconnectEnabled bool 41 pprofEnabled bool 42 } 43 44 func NewMux(opts MuxOpts) *Mux { 45 router := http.NewServeMux() 46 47 router.Handle("/", defaultHandler()) 48 49 promHandler := promhttp.InstrumentMetricHandler( 50 opts.PromRegisterer, 51 promhttp.HandlerFor(opts.PromRegisterer, 52 promhttp.HandlerOpts{ 53 Registry: opts.PromRegisterer, 54 }), 55 ) 56 57 router.Handle("/metrics", promHandler) 58 59 isReady := &atomic.Value{} 60 isReady.Store(false) 61 router.Handle("/ready", opts.isReady) 62 63 if opts.changeLogLevelEnabled { 64 router.Handle("/logger", loggerHandler(opts.Logger)) 65 } 66 67 // disconnect this agent from the API 68 if opts.disconnectEnabled { 69 router.Handle("/disconnect", http.HandlerFunc(disconnectHandler)) 70 } 71 72 // Register pprof handlers 73 if opts.pprofEnabled { 74 router.HandleFunc("/debug/pprof/", pprof.Index) 75 router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 76 router.HandleFunc("/debug/pprof/profile", pprof.Profile) 77 router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 78 router.HandleFunc("/debug/pprof/trace", pprof.Trace) 79 } 80 81 requestDurationVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{ 82 Namespace: "http", 83 Subsystem: "requests", 84 Name: "duration_seconds", 85 Help: "request duration", 86 }, []string{ 87 "code", 88 "method", 89 }) 90 91 if err := opts.PromRegisterer.Register(requestDurationVec); err != nil { 92 return nil 93 } 94 95 requestWrittenBytesVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{ 96 Namespace: "http", 97 Subsystem: "requests", 98 Name: "written_bytes", 99 Help: "total number of bytes written by code and method", 100 }, []string{ 101 "code", 102 "method", 103 }) 104 105 if err := opts.PromRegisterer.Register(requestWrittenBytesVec); err != nil { 106 return nil 107 } 108 109 inFlightRequests := prometheus.NewGauge(prometheus.GaugeOpts{ 110 Namespace: "http", 111 Subsystem: "requests", 112 Name: "in_flight", 113 Help: "number of requests in flight", 114 }) 115 116 if err := opts.PromRegisterer.Register(inFlightRequests); err != nil { 117 return nil 118 } 119 120 return &Mux{ 121 logger: opts.Logger, 122 router: router, 123 metrics: metrics{ 124 inFlightRequests: inFlightRequests, 125 requestDurationVec: requestDurationVec, 126 requestWrittenBytesVec: requestWrittenBytesVec, 127 }, 128 } 129 } 130 131 func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 132 mux.metrics.inFlightRequests.Inc() 133 m := httpsnoop.CaptureMetrics(mux.router, w, r) 134 mux.metrics.inFlightRequests.Dec() 135 136 mux.metrics.requestDurationVec.With(prometheus.Labels{ 137 "code": strconv.Itoa(m.Code), 138 "method": r.Method, 139 }).Observe(m.Duration.Seconds()) 140 141 mux.metrics.requestWrittenBytesVec.With(prometheus.Labels{ 142 "code": strconv.Itoa(m.Code), 143 "method": r.Method, 144 }).Observe(float64(m.Written)) 145 146 var level zerolog.Level 147 148 switch m.Code / 100 { 149 case 1, 2, 3: 150 level = zerolog.InfoLevel 151 case 4: 152 level = zerolog.WarnLevel 153 case 5: 154 level = zerolog.ErrorLevel 155 default: 156 level = zerolog.WarnLevel 157 } 158 159 mux.logger.WithLevel(level). 160 Str("method", r.Method). 161 Stringer("url", r.URL). 162 Int("code", m.Code). 163 Dur("duration", m.Duration). 164 Int64("bytes_written", m.Written). 165 Msg("handled request") 166 } 167 168 func defaultHandler() http.Handler { 169 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 170 if r.URL.Path != "/" { 171 http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 172 return 173 } 174 175 w.WriteHeader(http.StatusOK) 176 177 fmt.Fprintln(w, "hello, world!") 178 }) 179 } 180 181 // readynessHandler records whether the agent is ready to serve requests. 182 // 183 // readyness is defined by calling the method Set(true) on the handler 184 // at least once. Once the ready state is set, the handler never goes 185 // back to the unready state. 186 type readynessHandler int32 187 188 // NewReadynessHandler returns a new readynessHandler set to the unready 189 // state. 190 func NewReadynessHandler() *readynessHandler { 191 return new(readynessHandler) 192 } 193 194 // Set should be called once with an argument of true to indicate that 195 // the agent is ready to serve requests. Calling it again, no matter the 196 // value of the argument, has no effect. 197 func (h *readynessHandler) Set(v bool) { 198 if v { 199 atomic.StoreInt32((*int32)(h), 1) 200 } 201 } 202 203 // ServeHTTP implements http.Handler. 204 func (h *readynessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 205 if atomic.LoadInt32((*int32)(h)) == 0 { 206 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 207 return 208 } 209 210 // Signal readiness when the agent has connected once to the API. 211 w.WriteHeader(http.StatusOK) 212 fmt.Fprint(w, "ready") 213 } 214 215 // disconnectHandler triggers a disconnection from the API. 216 func disconnectHandler(w http.ResponseWriter, r *http.Request) { 217 // TODO(mem): this is a hack to trigger a disconnection from the 218 // API, it would be cleaner to do this through a channel that 219 // communicates the request to the checks updater. 220 p, err := os.FindProcess(os.Getpid()) 221 if err != nil { 222 msg := fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()) 223 http.Error(w, msg, http.StatusInternalServerError) 224 225 return 226 } 227 228 // SIGUSR1 will disconnect agent from API for 1 minute 229 err = p.Signal(syscall.SIGUSR1) 230 if err != nil { 231 msg := fmt.Sprintf("%s: %s", http.StatusText(http.StatusInternalServerError), err.Error()) 232 http.Error(w, msg, http.StatusInternalServerError) 233 234 return 235 } 236 237 w.WriteHeader(http.StatusOK) 238 fmt.Fprintln(w, "disconnecting this agent from the API for 1 minute.") 239 } 240 241 func loggerHandler(logger zerolog.Logger) http.Handler { 242 defaultLevel := logger.GetLevel() 243 244 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 245 if r.Method != http.MethodPost { 246 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 247 return 248 } 249 250 level, err := io.ReadAll(&io.LimitedReader{R: r.Body, N: 10}) 251 if err != nil { 252 http.Error(w, err.Error(), http.StatusInternalServerError) 253 return 254 } 255 256 // Consume the rest of the request if there's anything there. 257 _, _ = io.Copy(io.Discard, r.Body) 258 259 _ = r.Body.Close() 260 261 switch strings.TrimSpace(strings.ToLower(string(level))) { 262 case "debug": 263 zerolog.SetGlobalLevel(zerolog.DebugLevel) 264 logger.Warn().Stringer("level", zerolog.DebugLevel).Msg("changed log level") 265 266 case "default": 267 zerolog.SetGlobalLevel(defaultLevel) 268 logger.Warn().Stringer("level", defaultLevel).Msg("changed log level") 269 270 default: 271 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 272 } 273 }) 274 }