-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathwrap.go
More file actions
170 lines (156 loc) · 5.69 KB
/
wrap.go
File metadata and controls
170 lines (156 loc) · 5.69 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
170
package govisual
import (
"context"
"log"
"net"
"net/http"
"strings"
"time"
"github.com/doganarif/govisual/internal/dashboard"
"github.com/doganarif/govisual/internal/middleware"
"github.com/doganarif/govisual/internal/profiling"
"github.com/doganarif/govisual/internal/store"
"github.com/doganarif/govisual/internal/telemetry"
)
// Wrap wraps an http.Handler with the govisual request visualization middleware
// and mounts the dashboard at config.DashboardPath. Pass options to customize
// behavior. To trigger graceful shutdown of storage and telemetry resources,
// pass WithShutdownContext — govisual will release its resources when that
// context is cancelled. Govisual deliberately does NOT register a signal
// handler; that is the host application's job.
func Wrap(handler http.Handler, opts ...Option) http.Handler {
config := defaultConfig()
for _, opt := range opts {
opt(config)
}
var requestStore store.Store
storeConfig := &store.StorageConfig{
Type: config.StorageType,
Capacity: config.MaxRequests,
ConnectionString: config.ConnectionString,
TableName: config.TableName,
TTL: config.RedisTTL,
ExistingDB: config.ExistingDB,
}
rs, err := store.NewStore(storeConfig)
if err != nil {
log.Printf("govisual: failed to create configured storage backend: %v. Falling back to in-memory storage.", err)
requestStore = store.NewInMemoryStore(config.MaxRequests)
} else {
requestStore = rs
}
var profiler *profiling.Profiler
if config.EnableProfiling {
profiler = profiling.NewProfiler(config.MaxProfileMetrics)
profiler.SetEnabled(true)
profiler.SetProfileType(config.ProfileType)
profiler.SetThreshold(config.ProfileThreshold)
log.Printf("govisual: performance profiling enabled (threshold=%v)", config.ProfileThreshold)
}
var wrapped http.Handler
if profiler != nil {
wrapped = middleware.WrapWithProfilingAndLimits(
handler, requestStore,
config.LogRequestBody, config.LogResponseBody,
config, profiler, config.effectiveMaxBody(),
)
} else {
wrapped = middleware.WrapWithLimits(
handler, requestStore,
config.LogRequestBody, config.LogResponseBody,
config, config.effectiveMaxBody(),
)
}
var otelShutdown func(context.Context) error
if config.EnableOpenTelemetry {
ctx := context.Background()
otelConfig := telemetry.Config{
ServiceName: config.ServiceName,
ServiceVersion: config.ServiceVersion,
Endpoint: config.OTelEndpoint,
Insecure: config.OTelInsecure,
Exporter: config.OTelExporter,
}
shutdown, err := telemetry.InitTracer(ctx, otelConfig)
if err != nil {
log.Printf("govisual: failed to initialize OpenTelemetry: %v", err)
} else {
log.Printf("govisual: OpenTelemetry initialized (service=%s endpoint=%s)", config.ServiceName, config.OTelEndpoint)
otelShutdown = shutdown
wrapped = middleware.NewOTelMiddleware(wrapped, config.ServiceName, config.ServiceVersion)
}
}
if config.ShutdownContext != nil {
// NOTE: this goroutine waits on ctx.Done() and is retained for the
// process lifetime if the context is never cancelled. Callers passing a
// non-cancellable context (e.g. context.Background()) should be aware
// of this — in tests, prefer t.Context() or a cancellable context.
go func(ctx context.Context, st store.Store, shutdown func(context.Context) error) {
<-ctx.Done()
log.Printf("govisual: shutdown context cancelled, releasing resources")
if shutdown != nil {
// Give OTel a real deadline to flush spans, independent of
// the parent context (which is already cancelled).
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := shutdown(shutdownCtx); err != nil {
log.Printf("govisual: error shutting down OpenTelemetry: %v", err)
}
cancel()
}
if err := st.Close(); err != nil {
log.Printf("govisual: error closing storage: %v", err)
}
}(config.ShutdownContext, requestStore, otelShutdown)
}
dashHandler := dashboard.NewHandler(requestStore, profiler, dashboard.HandlerOptions{
EnableReplay: config.EnableReplay,
ExposeSystemInfo: config.ExposeSystemInfo,
ExposeEnvVars: config.ExposeEnvVars,
})
guardedDash := guardDashboard(dashHandler, config)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, config.DashboardPath) {
http.StripPrefix(config.DashboardPath, guardedDash).ServeHTTP(w, r)
return
}
wrapped.ServeHTTP(w, r)
})
}
// guardDashboard wraps the dashboard handler with localhost-only and
// authentication checks per the configuration.
func guardDashboard(h http.Handler, config *Config) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.LocalhostOnly && !isLoopback(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if config.DashboardAuth != nil && !config.DashboardAuth(r) {
// Surface a Basic challenge so browsers prompt the user; harmless
// when a custom auth scheme is in use.
w.Header().Set("WWW-Authenticate", `Basic realm="govisual"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
// isLoopback reports whether the request's remote address is a loopback IP.
func isLoopback(r *http.Request) bool {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
return ip.IsLoopback()
}
// effectiveMaxBody resolves the configured MaxBodyBytes against the package
// default. 0 means "use default"; negative means "no cap".
func (c *Config) effectiveMaxBody() int {
if c.MaxBodyBytes == 0 {
return middleware.DefaultMaxBodyBytes
}
return c.MaxBodyBytes
}