Add span based timings measurement and Sentry integration.

This commit is contained in:
Catherine
2025-09-30 00:56:58 +00:00
parent b1c50c10de
commit 1a0e594624
7 changed files with 123 additions and 41 deletions
+11
View File
@@ -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)
-----------------
+50 -35
View File
@@ -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
}
+2
View File
@@ -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)))
+4
View File
@@ -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)
+15 -5
View File
@@ -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,
+40
View File
@@ -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)
}
}
}
+1 -1
View File
@@ -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 {