mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-08-19 02:16:04 +00:00
Add span based timings measurement and Sentry integration.
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user