155 lines
4.1 KiB
Go
155 lines
4.1 KiB
Go
package observability
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Middleware counts HTTP completion once. A successful upgrade completes HTTP
|
|
// at Hijack; the still-running WebSocket handler is counted separately.
|
|
func (r *Registry) Middleware(next http.Handler) http.Handler {
|
|
if r == nil {
|
|
return next
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
started := time.Now()
|
|
route := routeClass(req.URL.Path)
|
|
r.AddGauge("orbit_http_requests_in_flight", 1, nil)
|
|
cw := &captureWriter{ResponseWriter: w}
|
|
completed := false
|
|
ws := false
|
|
complete := func(status int) {
|
|
if completed {
|
|
return
|
|
}
|
|
completed = true
|
|
class := "other"
|
|
if status >= 100 && status < 600 {
|
|
class = strconv.Itoa(status/100) + "xx"
|
|
}
|
|
labels := Labels{"method": boundedMethod(req.Method), "route": route, "status_class": class}
|
|
r.AddGauge("orbit_http_requests_in_flight", -1, nil)
|
|
r.AddCounter("orbit_http_requests_total", 1, labels)
|
|
r.ObserveHistogram("orbit_http_request_duration_seconds", time.Since(started).Seconds(), labels)
|
|
}
|
|
cw.onHijack = func() {
|
|
complete(cw.status)
|
|
if cw.status == 101 && (route == "worker_connect" || route == "event_connect") {
|
|
ws = true
|
|
r.AddGauge("orbit_websocket_connections", 1, Labels{"route": route})
|
|
r.AddCounter("orbit_websocket_connections_total", 1, Labels{"route": route})
|
|
}
|
|
}
|
|
returned := false
|
|
defer func() {
|
|
status := cw.status
|
|
if !returned {
|
|
r.AddCounter("orbit_http_panics_total", 1, nil)
|
|
if status == 0 {
|
|
status = 500
|
|
}
|
|
}
|
|
if status == 0 {
|
|
status = 200
|
|
}
|
|
complete(status)
|
|
if ws {
|
|
r.AddGauge("orbit_websocket_connections", -1, Labels{"route": route})
|
|
}
|
|
// No recover: net/http retains its panic/ErrAbortHandler behavior.
|
|
}()
|
|
next.ServeHTTP(cw, req)
|
|
returned = true
|
|
})
|
|
}
|
|
func boundedMethod(method string) string {
|
|
for _, v := range methods {
|
|
if method == v {
|
|
return v
|
|
}
|
|
}
|
|
return "OTHER"
|
|
}
|
|
func routeClass(path string) string {
|
|
switch path {
|
|
case "/health/live":
|
|
return "health_live"
|
|
case "/health/ready":
|
|
return "health_ready"
|
|
case "/v1/worker/connect":
|
|
return "worker_connect"
|
|
case "/v1/events/connect":
|
|
return "event_connect"
|
|
}
|
|
for _, p := range []struct{ prefix, route string }{
|
|
{"/v1/tasks", "tasks"}, {"/v1/queues", "queues"}, {"/v1/workflows", "workflows"},
|
|
{"/v1/workflow-definitions", "workflow_definitions"}, {"/v1/schedules", "schedules"},
|
|
{"/v1/subscriptions", "subscriptions"}, {"/v1/application/resources", "application_resources"},
|
|
} {
|
|
if path == p.prefix || strings.HasPrefix(path, p.prefix+"/") || strings.HasPrefix(path, p.prefix+":") {
|
|
return p.route
|
|
}
|
|
}
|
|
return "other"
|
|
}
|
|
|
|
type captureWriter struct {
|
|
http.ResponseWriter
|
|
status int
|
|
onHijack func()
|
|
}
|
|
|
|
func (w *captureWriter) WriteHeader(status int) {
|
|
if w.status != 0 {
|
|
return
|
|
}
|
|
// Forward informational responses without preventing a later final response.
|
|
if status >= 100 && status < 200 && status != 101 {
|
|
w.ResponseWriter.WriteHeader(status)
|
|
return
|
|
}
|
|
w.ResponseWriter.WriteHeader(status)
|
|
w.status = status
|
|
}
|
|
func (w *captureWriter) Write(p []byte) (int, error) {
|
|
if w.status == 0 {
|
|
w.WriteHeader(200)
|
|
}
|
|
return w.ResponseWriter.Write(p)
|
|
}
|
|
func (w *captureWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter }
|
|
func (w *captureWriter) FlushError() error {
|
|
if w.status == 0 {
|
|
w.WriteHeader(200)
|
|
}
|
|
return http.NewResponseController(w.ResponseWriter).Flush()
|
|
}
|
|
func (w *captureWriter) Flush() { _ = w.FlushError() }
|
|
func (w *captureWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
c, b, err := http.NewResponseController(w.ResponseWriter).Hijack()
|
|
if err == nil && w.onHijack != nil {
|
|
w.onHijack()
|
|
}
|
|
return c, b, err
|
|
}
|
|
func (w *captureWriter) Push(target string, opts *http.PushOptions) error {
|
|
if p, ok := w.ResponseWriter.(http.Pusher); ok {
|
|
return p.Push(target, opts)
|
|
}
|
|
return http.ErrNotSupported
|
|
}
|
|
func (w *captureWriter) ReadFrom(src io.Reader) (int64, error) {
|
|
if w.status == 0 {
|
|
w.WriteHeader(200)
|
|
}
|
|
if rf, ok := w.ResponseWriter.(io.ReaderFrom); ok {
|
|
return rf.ReadFrom(src)
|
|
}
|
|
return io.Copy(w.ResponseWriter, src)
|
|
}
|