From 1a0e59462434f594f90633035515073b5c4f6777 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 30 Sep 2025 00:56:58 +0000 Subject: [PATCH] Add span based timings measurement and Sentry integration. --- README.md | 11 ++++++ src/backend_observer.go | 85 ++++++++++++++++++++++++----------------- src/backend_s3.go | 2 + src/fetch.go | 4 ++ src/manifest.go | 20 +++++++--- src/observe.go | 40 +++++++++++++++++++ src/update.go | 2 +- 7 files changed, 123 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3dc2a8b..8c2aaaa 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,17 @@ The authorization flow for metadata retrieval (`GET` requests with site paths st 4. **Default Deny:** Otherwise, the request is not authorized. +Observability +------------- + +_git-pages_ has robust observability features built in: +* The metrics endpoint (bound to `:3002` by default) returns Go, pages server, and storage backend metrics in the [Prometheus](https://prometheus.io/) format. +* Optional [Sentry](https://sentry.io/) integration allows greater visibility into the application. The `ENVIRONMENT` environment variable configures the deploy environment name (`development` by default). + * If `SENTRY_DSN` environment variable is set, panics are reported to Sentry. + * If `SENTRY_DSN` and `SENTRY_LOGS=1` environment variables are set, logs are uploaded to Sentry. + * If `SENTRY_DSN` and `SENTRY_TRACING=1` environment variables are set, traces are uploaded to Sentry. + + Architecture (v2) ----------------- diff --git a/src/backend_observer.go b/src/backend_observer.go index 4bb2c1f..2cfd11e 100644 --- a/src/backend_observer.go +++ b/src/backend_observer.go @@ -35,16 +35,16 @@ var ( ) type observedBackend struct { - backend Backend + inner Backend } var _ Backend = (*observedBackend)(nil) func NewObservedBackend(backend Backend) Backend { - return &observedBackend{backend: backend} + return &observedBackend{inner: backend} } -func (b *observedBackend) GetBlob( +func (backend *observedBackend) GetBlob( ctx context.Context, name string, ) ( @@ -53,50 +53,65 @@ func (b *observedBackend) GetBlob( mtime time.Time, err error, ) { - reader, size, mtime, err = b.backend.GetBlob(ctx, name) - if err != nil { - return + span, ctx := ObserveFunction(ctx, "GetBlob", "blob.name", name) + if reader, size, mtime, err = backend.inner.GetBlob(ctx, name); err == nil { + blobsRetrievedCount.Inc() + blobsRetrievedBytes.Add(float64(size)) } - blobsRetrievedCount.Inc() - blobsRetrievedBytes.Add(float64(size)) + span.Finish() return } -func (b *observedBackend) PutBlob(ctx context.Context, name string, data []byte) error { - err := b.backend.PutBlob(ctx, name, data) - if err != nil { - return err +func (backend *observedBackend) PutBlob(ctx context.Context, name string, data []byte) (err error) { + span, ctx := ObserveFunction(ctx, "PutBlob", "blob.name", name, "blob.size", len(data)) + if err = backend.inner.PutBlob(ctx, name, data); err == nil { + blobsStoredCount.Inc() + blobsStoredBytes.Add(float64(len(data))) } - blobsStoredCount.Inc() - blobsStoredBytes.Add(float64(len(data))) - return nil -} - -func (b *observedBackend) DeleteBlob(ctx context.Context, name string) error { - return b.backend.DeleteBlob(ctx, name) -} - -func (b *observedBackend) GetManifest(ctx context.Context, name string) (manifest *Manifest, err error) { - manifest, err = b.backend.GetManifest(ctx, name) - if err != nil { - return - } - manifestsRetrievedCount.Inc() + span.Finish() return } -func (b *observedBackend) StageManifest(ctx context.Context, manifest *Manifest) error { - return b.backend.StageManifest(ctx, manifest) +func (backend *observedBackend) DeleteBlob(ctx context.Context, name string) (err error) { + span, ctx := ObserveFunction(ctx, "DeleteBlob", "blob.name", name) + err = backend.inner.DeleteBlob(ctx, name) + span.Finish() + return } -func (b *observedBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error { - return b.backend.CommitManifest(ctx, name, manifest) +func (backend *observedBackend) GetManifest(ctx context.Context, name string) (manifest *Manifest, err error) { + span, ctx := ObserveFunction(ctx, "GetManifest", "manifest.name", name) + if manifest, err = backend.inner.GetManifest(ctx, name); err == nil { + manifestsRetrievedCount.Inc() + } + span.Finish() + return } -func (b *observedBackend) DeleteManifest(ctx context.Context, name string) error { - return b.backend.DeleteManifest(ctx, name) +func (backend *observedBackend) StageManifest(ctx context.Context, manifest *Manifest) (err error) { + span, ctx := ObserveFunction(ctx, "StageManifest") + err = backend.inner.StageManifest(ctx, manifest) + span.Finish() + return } -func (b *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) { - return b.backend.CheckDomain(ctx, domain) +func (backend *observedBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) (err error) { + span, ctx := ObserveFunction(ctx, "CommitManifest", "manifest.name", name) + err = backend.inner.CommitManifest(ctx, name, manifest) + span.Finish() + return +} + +func (backend *observedBackend) DeleteManifest(ctx context.Context, name string) (err error) { + span, ctx := ObserveFunction(ctx, "DeleteManifest", "manifest.name", name) + err = backend.inner.DeleteManifest(ctx, name) + span.Finish() + return +} + +func (backend *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) { + span, ctx := ObserveFunction(ctx, "CheckDomain", "manifest.domain", domain) + found, err = backend.inner.CheckDomain(ctx, domain) + span.Finish() + return } diff --git a/src/backend_s3.go b/src/backend_s3.go index 2e9afa6..eee9120 100644 --- a/src/backend_s3.go +++ b/src/backend_s3.go @@ -252,6 +252,7 @@ func (s3 *S3Backend) PutBlob(ctx context.Context, name string, data []byte) erro if err != nil { return err } else { + ObserveData(ctx, "blob.status", "created") log.Printf("s3: put blob %s (created)\n", name) return nil } @@ -259,6 +260,7 @@ func (s3 *S3Backend) PutBlob(ctx context.Context, name string, data []byte) erro return err } } else { + ObserveData(ctx, "blob.status", "exists") log.Printf("s3: put blob %s (exists)\n", name) blobsDedupedCount.Inc() blobsDedupedBytes.Add(float64(len(data))) diff --git a/src/fetch.go b/src/fetch.go index f89ecb8..dcb66bc 100644 --- a/src/fetch.go +++ b/src/fetch.go @@ -17,6 +17,10 @@ import ( ) func FetchRepository(ctx context.Context, repoURL string, branch string) (*Manifest, error) { + span, ctx := ObserveFunction(ctx, "FetchRepository", + "git.repository", repoURL, "git.branch", branch) + defer span.Finish() + baseDir, err := os.MkdirTemp("", "fetchRepo") if err != nil { return nil, fmt.Errorf("mkdtemp: %w", err) diff --git a/src/manifest.go b/src/manifest.go index 502d6bd..5cdea3d 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -122,14 +122,20 @@ again: } } +// The `clauspost/compress/zstd` package recommends reusing a compressor to avoid repeated +// allocations of internal buffers. +var zstdEncoder, _ = zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBetterCompression)) + // Compress contents of inline files. -func CompressFiles(manifest *Manifest) { +func CompressFiles(ctx context.Context, manifest *Manifest) { + span, _ := ObserveFunction(ctx, "CompressFiles") + defer span.Finish() + var originalSize, transformedSize uint32 - var encoder, _ = zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBetterCompression)) for _, entry := range manifest.Contents { if entry.GetType() == Type_InlineFile && entry.GetXfrm() == Transform_None { originalSize += entry.GetSize() - compressedData := encoder.EncodeAll(entry.GetData(), make([]byte, 0, entry.GetSize())) + compressedData := zstdEncoder.EncodeAll(entry.GetData(), make([]byte, 0, entry.GetSize())) if len(compressedData) < int(*entry.Size) { entry.Data = compressedData entry.Size = proto.Uint32(uint32(len(entry.Data))) @@ -138,6 +144,7 @@ func CompressFiles(manifest *Manifest) { transformedSize += entry.GetSize() } } + log.Printf("compress: saved %.2f%% (%s to %s)", (float32(originalSize)-float32(transformedSize))/float32(originalSize)*100.0, datasize.ByteSize(originalSize).HR(), @@ -148,7 +155,7 @@ func CompressFiles(manifest *Manifest) { // Apply post-processing steps to the manifest. // At the moment, there isn't a good way to report errors except to log them on the terminal. // (Perhaps in the future they could be exposed at `.git-pages/status.txt`?) -func PrepareManifest(manifest *Manifest) error { +func PrepareManifest(ctx context.Context, manifest *Manifest) error { // Parse Netlify-style `_redirects` if err := ProcessRedirects(manifest); err != nil { log.Printf("redirects err: %s\n", err) @@ -157,7 +164,7 @@ func PrepareManifest(manifest *Manifest) error { } if config.Feature("compress") { - CompressFiles(manifest) + CompressFiles(ctx, manifest) } return nil @@ -168,6 +175,9 @@ var ErrManifestTooLarge = errors.New("manifest too large") // Uploads inline file data over certain size to the storage backend. Returns a copy of // the manifest updated to refer to an external content-addressable store. func StoreManifest(ctx context.Context, name string, manifest *Manifest) (*Manifest, error) { + span, ctx := ObserveFunction(ctx, "StoreManifest", "manifest.name", name) + defer span.Finish() + // Replace inline files over certain size with references to external data. extManifest := Manifest{ RepoUrl: manifest.RepoUrl, diff --git a/src/observe.go b/src/observe.go index bef4191..dbe338e 100644 --- a/src/observe.go +++ b/src/observe.go @@ -61,9 +61,20 @@ func InitObservability() { enableLogs = value } + enableTracing := false + if value, err := strconv.ParseBool(os.Getenv("SENTRY_TRACING")); err == nil { + enableTracing = value + } + options := sentry.ClientOptions{} options.Environment = environment options.EnableLogs = enableLogs + options.EnableTracing = enableTracing + if environment == "development" { + options.TracesSampleRate = 1.0 + } else { + options.TracesSampleRate = 60.0 + } if err := sentry.Init(options); err != nil { log.Fatalf("sentry: %s\n", err) } @@ -97,3 +108,32 @@ func ObserveHTTPHandler(handler http.Handler) http.Handler { return handler } + +type noopSpan struct{} + +func (span noopSpan) Finish() {} + +func ObserveFunction( + ctx context.Context, funcName string, data ...any, +) ( + interface{ Finish() }, context.Context, +) { + switch { + case hasSentry(): + span := sentry.StartSpan(ctx, "function") + span.Description = funcName + ObserveData(span.Context(), data...) + return span, span.Context() + default: + return noopSpan{}, ctx + } +} + +func ObserveData(ctx context.Context, data ...any) { + if span := sentry.SpanFromContext(ctx); span != nil { + for i := 0; i < len(data); i += 2 { + name, value := data[i], data[i+1] + span.SetData(name.(string), value) + } + } +} diff --git a/src/update.go b/src/update.go index 30c528c..240c982 100644 --- a/src/update.go +++ b/src/update.go @@ -40,7 +40,7 @@ func Update(ctx context.Context, webRoot string, manifest *Manifest) UpdateResul outcome = UpdateDeleted } } - } else if err = PrepareManifest(manifest); err == nil { + } else if err = PrepareManifest(ctx, manifest); err == nil { newManifest, err = StoreManifest(ctx, webRoot, manifest) if err == nil { if oldManifest == nil {