[mirror] Scalable static site server for Git forges (like GitHub Pages)
at main 14 kB view raw
1package git_pages 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "iter" 9 "log" 10 "log/slog" 11 "math/rand/v2" 12 "net/http" 13 "os" 14 "runtime/debug" 15 "strconv" 16 "sync" 17 "time" 18 19 slogmulti "github.com/samber/slog-multi" 20 21 syslog "codeberg.org/git-pages/go-slog-syslog" 22 23 "github.com/prometheus/client_golang/prometheus" 24 "github.com/prometheus/client_golang/prometheus/promauto" 25 26 "github.com/getsentry/sentry-go" 27 sentryhttp "github.com/getsentry/sentry-go/http" 28 sentryslog "github.com/getsentry/sentry-go/slog" 29) 30 31var ( 32 httpRequestCount = promauto.NewCounterVec(prometheus.CounterOpts{ 33 Name: "git_pages_http_request_count", 34 Help: "Count of HTTP requests by method and response status code", 35 }, []string{"method", "code"}) 36 37 httpRequestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ 38 Name: "git_pages_http_request_duration_seconds", 39 Help: "Time to respond to incoming HTTP requests", 40 Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, 41 42 NativeHistogramBucketFactor: 1.1, 43 NativeHistogramMaxBucketNumber: 100, 44 NativeHistogramMinResetDuration: 10 * time.Minute, 45 }, []string{"method"}) 46) 47 48var syslogHandler syslog.Handler 49 50func hasSentry() bool { 51 return os.Getenv("SENTRY_DSN") != "" 52} 53 54func chainSentryMiddleware( 55 middleware ...func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event, 56) func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 57 return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 58 for idx := 0; idx < len(middleware) && event != nil; idx++ { 59 event = middleware[idx](event, hint) 60 } 61 return event 62 } 63} 64 65// sensitiveHTTPHeaders extends the list of sensitive headers defined in the Sentry Go SDK with our 66// own application-specific header field names. 67var sensitiveHTTPHeaders = map[string]struct{}{ 68 "Forge-Authorization": {}, 69} 70 71// scrubSentryEvent removes sensitive HTTP header fields from the Sentry event. 72func scrubSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 73 if event.Request != nil && event.Request.Headers != nil { 74 for key := range event.Request.Headers { 75 if _, ok := sensitiveHTTPHeaders[key]; ok { 76 delete(event.Request.Headers, key) 77 } 78 } 79 } 80 return event 81} 82 83// sampleSentryEvent returns a function that discards a Sentry event according to the sample rate, 84// unless the associated HTTP request triggers a mutation or it took too long to produce a response, 85// in which case the event is never discarded. 86func sampleSentryEvent(sampleRate float64) func(*sentry.Event, *sentry.EventHint) *sentry.Event { 87 return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 88 newSampleRate := sampleRate 89 if event.Request != nil { 90 switch event.Request.Method { 91 case "PUT", "POST", "DELETE": 92 newSampleRate = 1 93 } 94 } 95 duration := event.Timestamp.Sub(event.StartTime) 96 threshold := time.Duration(config.Observability.SlowResponseThreshold) 97 if duration >= threshold { 98 newSampleRate = 1 99 } 100 if rand.Float64() < newSampleRate { 101 return event 102 } 103 return nil 104 } 105} 106 107func InitObservability() { 108 debug.SetPanicOnFault(true) 109 110 environment := "development" 111 if value, ok := os.LookupEnv("ENVIRONMENT"); ok { 112 environment = value 113 } 114 115 logHandlers := []slog.Handler{} 116 117 switch config.LogFormat { 118 case "none": 119 // nothing to do 120 case "text": 121 logHandlers = append(logHandlers, 122 slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) 123 case "json": 124 logHandlers = append(logHandlers, 125 slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{})) 126 default: 127 log.Println("unknown log format", config.LogFormat) 128 } 129 130 if syslogAddr := os.Getenv("SYSLOG_ADDR"); syslogAddr != "" { 131 var err error 132 syslogHandler, err = syslog.NewHandler(&syslog.HandlerOptions{ 133 Address: syslogAddr, 134 AppName: "git-pages", 135 StructuredDataID: "git-pages", 136 }) 137 if err != nil { 138 log.Fatalf("syslog: %v", err) 139 } 140 logHandlers = append(logHandlers, syslogHandler) 141 } 142 143 if hasSentry() { 144 enableLogs := false 145 if value, err := strconv.ParseBool(os.Getenv("SENTRY_LOGS")); err == nil { 146 enableLogs = value 147 } 148 149 enableTracing := false 150 if value, err := strconv.ParseBool(os.Getenv("SENTRY_TRACING")); err == nil { 151 enableTracing = value 152 } 153 154 tracesSampleRate := 1.00 155 switch environment { 156 case "development", "staging": 157 default: 158 tracesSampleRate = 0.05 159 } 160 161 options := sentry.ClientOptions{} 162 options.DisableTelemetryBuffer = !config.Feature("sentry-telemetry-buffer") 163 options.Environment = environment 164 options.EnableLogs = enableLogs 165 options.EnableTracing = enableTracing 166 options.TracesSampleRate = 1 // use our own custom sampling logic 167 options.BeforeSend = scrubSentryEvent 168 options.BeforeSendTransaction = chainSentryMiddleware( 169 sampleSentryEvent(tracesSampleRate), 170 scrubSentryEvent, 171 ) 172 if err := sentry.Init(options); err != nil { 173 log.Fatalf("sentry: %s\n", err) 174 } 175 176 if enableLogs { 177 logHandlers = append(logHandlers, sentryslog.Option{ 178 AddSource: true, 179 }.NewSentryHandler(context.Background())) 180 } 181 } 182 183 slog.SetDefault(slog.New(slogmulti.Fanout(logHandlers...))) 184} 185 186func FiniObservability() { 187 var wg sync.WaitGroup 188 timeout := 2 * time.Second 189 if syslogHandler != nil { 190 wg.Go(func() { syslogHandler.Flush(timeout) }) 191 } 192 if hasSentry() { 193 wg.Go(func() { sentry.Flush(timeout) }) 194 } 195 wg.Wait() 196} 197 198func ObserveError(err error) { 199 if errors.Is(err, context.Canceled) { 200 // Something has explicitly requested cancellation. 201 // Timeout results in a different error. 202 return 203 } 204 205 if hasSentry() { 206 sentry.CaptureException(err) 207 } 208} 209 210type observedResponseWriter struct { 211 inner http.ResponseWriter 212 status int 213} 214 215func newObservedResponseWriter(w http.ResponseWriter) observedResponseWriter { 216 return observedResponseWriter{ 217 inner: w, 218 status: 0, 219 } 220} 221 222func (w *observedResponseWriter) Unwrap() http.ResponseWriter { 223 return w.inner 224} 225 226func (w *observedResponseWriter) Header() http.Header { 227 return w.inner.Header() 228} 229 230func (w *observedResponseWriter) Write(data []byte) (int, error) { 231 return w.inner.Write(data) 232} 233 234func (w *observedResponseWriter) WriteHeader(statusCode int) { 235 w.status = statusCode 236 w.inner.WriteHeader(statusCode) 237} 238 239func ObserveHTTPHandler(handler http.Handler) http.Handler { 240 if hasSentry() { 241 handler = func(next http.Handler) http.Handler { 242 next = sentryhttp.New(sentryhttp.Options{ 243 Repanic: true, 244 }).Handle(handler) 245 246 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 // Prevent the Sentry SDK from continuing traces as we don't use this feature. 248 r.Header.Del(sentry.SentryTraceHeader) 249 r.Header.Del(sentry.SentryBaggageHeader) 250 251 next.ServeHTTP(w, r) 252 }) 253 }(handler) 254 } 255 256 handler = func(next http.Handler) http.Handler { 257 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 258 ow := newObservedResponseWriter(w) 259 260 start := time.Now() 261 next.ServeHTTP(&ow, r) 262 duration := time.Since(start) 263 264 httpRequestCount. 265 With(prometheus.Labels{"method": r.Method, "code": fmt.Sprintf("%d", ow.status)}). 266 Inc() 267 httpRequestDurationSeconds. 268 With(prometheus.Labels{"method": r.Method}). 269 Observe(duration.Seconds()) 270 }) 271 }(handler) 272 273 return handler 274} 275 276type noopSpan struct{} 277 278func (span noopSpan) Finish() {} 279 280func ObserveFunction( 281 ctx context.Context, funcName string, data ...any, 282) ( 283 interface{ Finish() }, context.Context, 284) { 285 switch { 286 case hasSentry(): 287 span := sentry.StartSpan(ctx, "function") 288 span.Description = funcName 289 ObserveData(span.Context(), data...) 290 return span, span.Context() 291 default: 292 return noopSpan{}, ctx 293 } 294} 295 296func ObserveData(ctx context.Context, data ...any) { 297 if span := sentry.SpanFromContext(ctx); span != nil { 298 for i := 0; i < len(data); i += 2 { 299 name, value := data[i], data[i+1] 300 span.SetData(name.(string), value) 301 } 302 } 303} 304 305var ( 306 blobsRetrievedCount = promauto.NewCounter(prometheus.CounterOpts{ 307 Name: "git_pages_blobs_retrieved", 308 Help: "Count of blobs retrieved", 309 }) 310 blobsRetrievedBytes = promauto.NewCounter(prometheus.CounterOpts{ 311 Name: "git_pages_blobs_retrieved_bytes", 312 Help: "Total size in bytes of blobs retrieved", 313 }) 314 315 blobsStoredCount = promauto.NewCounter(prometheus.CounterOpts{ 316 Name: "git_pages_blobs_stored", 317 Help: "Count of blobs stored", 318 }) 319 blobsStoredBytes = promauto.NewCounter(prometheus.CounterOpts{ 320 Name: "git_pages_blobs_stored_bytes", 321 Help: "Total size in bytes of blobs stored", 322 }) 323 324 manifestsRetrievedCount = promauto.NewCounter(prometheus.CounterOpts{ 325 Name: "git_pages_manifests_retrieved", 326 Help: "Count of manifests retrieved", 327 }) 328) 329 330type observedBackend struct { 331 inner Backend 332} 333 334var _ Backend = (*observedBackend)(nil) 335 336func NewObservedBackend(backend Backend) Backend { 337 return &observedBackend{inner: backend} 338} 339 340func (backend *observedBackend) HasFeature(ctx context.Context, feature BackendFeature) (isOn bool) { 341 span, ctx := ObserveFunction(ctx, "HasFeature") 342 isOn = backend.inner.HasFeature(ctx, feature) 343 span.Finish() 344 return 345} 346 347func (backend *observedBackend) EnableFeature(ctx context.Context, feature BackendFeature) (err error) { 348 span, ctx := ObserveFunction(ctx, "EnableFeature") 349 err = backend.inner.EnableFeature(ctx, feature) 350 span.Finish() 351 return 352} 353 354func (backend *observedBackend) GetBlob( 355 ctx context.Context, name string, 356) ( 357 reader io.ReadSeeker, metadata BlobMetadata, err error, 358) { 359 span, ctx := ObserveFunction(ctx, "GetBlob", "blob.name", name) 360 if reader, metadata, err = backend.inner.GetBlob(ctx, name); err == nil { 361 ObserveData(ctx, "blob.size", metadata.Size) 362 blobsRetrievedCount.Inc() 363 blobsRetrievedBytes.Add(float64(metadata.Size)) 364 } 365 span.Finish() 366 return 367} 368 369func (backend *observedBackend) PutBlob(ctx context.Context, name string, data []byte) (err error) { 370 span, ctx := ObserveFunction(ctx, "PutBlob", "blob.name", name, "blob.size", len(data)) 371 if err = backend.inner.PutBlob(ctx, name, data); err == nil { 372 blobsStoredCount.Inc() 373 blobsStoredBytes.Add(float64(len(data))) 374 } 375 span.Finish() 376 return 377} 378 379func (backend *observedBackend) DeleteBlob(ctx context.Context, name string) (err error) { 380 span, ctx := ObserveFunction(ctx, "DeleteBlob", "blob.name", name) 381 err = backend.inner.DeleteBlob(ctx, name) 382 span.Finish() 383 return 384} 385 386func (backend *observedBackend) EnumerateBlobs(ctx context.Context) iter.Seq2[BlobMetadata, error] { 387 return func(yield func(BlobMetadata, error) bool) { 388 span, ctx := ObserveFunction(ctx, "EnumerateBlobs") 389 for metadata, err := range backend.inner.EnumerateBlobs(ctx) { 390 if !yield(metadata, err) { 391 break 392 } 393 } 394 span.Finish() 395 } 396} 397 398func (backend *observedBackend) GetManifest( 399 ctx context.Context, name string, opts GetManifestOptions, 400) ( 401 manifest *Manifest, metadata ManifestMetadata, err error, 402) { 403 span, ctx := ObserveFunction(ctx, "GetManifest", 404 "manifest.name", name, 405 "manifest.bypass_cache", opts.BypassCache, 406 ) 407 if manifest, metadata, err = backend.inner.GetManifest(ctx, name, opts); err == nil { 408 manifestsRetrievedCount.Inc() 409 } 410 span.Finish() 411 return 412} 413 414func (backend *observedBackend) StageManifest(ctx context.Context, manifest *Manifest) (err error) { 415 span, ctx := ObserveFunction(ctx, "StageManifest") 416 err = backend.inner.StageManifest(ctx, manifest) 417 span.Finish() 418 return 419} 420 421func (backend *observedBackend) HasAtomicCAS(ctx context.Context) bool { 422 return backend.inner.HasAtomicCAS(ctx) 423} 424 425func (backend *observedBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest, opts ModifyManifestOptions) (err error) { 426 span, ctx := ObserveFunction(ctx, "CommitManifest", "manifest.name", name) 427 err = backend.inner.CommitManifest(ctx, name, manifest, opts) 428 span.Finish() 429 return 430} 431 432func (backend *observedBackend) DeleteManifest(ctx context.Context, name string, opts ModifyManifestOptions) (err error) { 433 span, ctx := ObserveFunction(ctx, "DeleteManifest", "manifest.name", name) 434 err = backend.inner.DeleteManifest(ctx, name, opts) 435 span.Finish() 436 return 437} 438 439func (backend *observedBackend) EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error] { 440 return func(yield func(ManifestMetadata, error) bool) { 441 span, ctx := ObserveFunction(ctx, "EnumerateManifests") 442 for metadata, err := range backend.inner.EnumerateManifests(ctx) { 443 if !yield(metadata, err) { 444 break 445 } 446 } 447 span.Finish() 448 } 449} 450 451func (backend *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) { 452 span, ctx := ObserveFunction(ctx, "CheckDomain", "domain.name", domain) 453 found, err = backend.inner.CheckDomain(ctx, domain) 454 span.Finish() 455 return 456} 457 458func (backend *observedBackend) CreateDomain(ctx context.Context, domain string) (err error) { 459 span, ctx := ObserveFunction(ctx, "CreateDomain", "domain.name", domain) 460 err = backend.inner.CreateDomain(ctx, domain) 461 span.Finish() 462 return 463} 464 465func (backend *observedBackend) FreezeDomain(ctx context.Context, domain string) (err error) { 466 span, ctx := ObserveFunction(ctx, "FreezeDomain", "domain.name", domain) 467 err = backend.inner.FreezeDomain(ctx, domain) 468 span.Finish() 469 return 470} 471 472func (backend *observedBackend) UnfreezeDomain(ctx context.Context, domain string) (err error) { 473 span, ctx := ObserveFunction(ctx, "UnfreezeDomain", "domain.name", domain) 474 err = backend.inner.UnfreezeDomain(ctx, domain) 475 span.Finish() 476 return 477} 478 479func (backend *observedBackend) AppendAuditLog(ctx context.Context, id AuditID, record *AuditRecord) (err error) { 480 span, ctx := ObserveFunction(ctx, "AppendAuditLog", "audit.id", id) 481 err = backend.inner.AppendAuditLog(ctx, id, record) 482 span.Finish() 483 return 484} 485 486func (backend *observedBackend) QueryAuditLog(ctx context.Context, id AuditID) (record *AuditRecord, err error) { 487 span, ctx := ObserveFunction(ctx, "QueryAuditLog", "audit.id", id) 488 record, err = backend.inner.QueryAuditLog(ctx, id) 489 span.Finish() 490 return 491} 492 493func (backend *observedBackend) SearchAuditLog( 494 ctx context.Context, opts SearchAuditLogOptions, 495) iter.Seq2[AuditID, error] { 496 return func(yield func(AuditID, error) bool) { 497 span, ctx := ObserveFunction(ctx, "SearchAuditLog", 498 "audit.search.since", opts.Since, 499 "audit.search.until", opts.Until, 500 ) 501 for id, err := range backend.inner.SearchAuditLog(ctx, opts) { 502 if !yield(id, err) { 503 break 504 } 505 } 506 span.Finish() 507 } 508}