mirror of
https://github.com/go-gitea/gitea
synced 2025-01-21 23:24:29 +00:00
7069369e03
1. Add a OpenTelemetry-like shim-layer to collect traces 2. Add a simple builtin trace collector and exporter, end users could download the diagnosis report to get the traces. This PR's design is quite lightweight, no hard-dependency, and it is easy to improve or remove. We can try it on gitea.com first to see whether it works well, and fine tune the details. --------- Co-authored-by: silverwind <me@silverwind.io>
176 lines
4.0 KiB
Go
176 lines
4.0 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package gtprof
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
type contextKey struct {
|
|
name string
|
|
}
|
|
|
|
var contextKeySpan = &contextKey{"span"}
|
|
|
|
type traceStarter interface {
|
|
start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal)
|
|
}
|
|
|
|
type traceSpanInternal interface {
|
|
addEvent(name string, cfg *EventConfig)
|
|
recordError(err error, cfg *EventConfig)
|
|
end()
|
|
}
|
|
|
|
type TraceSpan struct {
|
|
// immutable
|
|
parent *TraceSpan
|
|
internalSpans []traceSpanInternal
|
|
internalContexts []context.Context
|
|
|
|
// mutable, must be protected by mutex
|
|
mu sync.RWMutex
|
|
name string
|
|
statusCode uint32
|
|
statusDesc string
|
|
startTime time.Time
|
|
endTime time.Time
|
|
attributes []*TraceAttribute
|
|
children []*TraceSpan
|
|
}
|
|
|
|
type TraceAttribute struct {
|
|
Key string
|
|
Value TraceValue
|
|
}
|
|
|
|
type TraceValue struct {
|
|
v any
|
|
}
|
|
|
|
func (t *TraceValue) AsString() string {
|
|
return fmt.Sprint(t.v)
|
|
}
|
|
|
|
func (t *TraceValue) AsInt64() int64 {
|
|
v, _ := util.ToInt64(t.v)
|
|
return v
|
|
}
|
|
|
|
func (t *TraceValue) AsFloat64() float64 {
|
|
v, _ := util.ToFloat64(t.v)
|
|
return v
|
|
}
|
|
|
|
var globalTraceStarters []traceStarter
|
|
|
|
type Tracer struct {
|
|
starters []traceStarter
|
|
}
|
|
|
|
func (s *TraceSpan) SetName(name string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.name = name
|
|
}
|
|
|
|
func (s *TraceSpan) SetStatus(code uint32, desc string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.statusCode, s.statusDesc = code, desc
|
|
}
|
|
|
|
func (s *TraceSpan) AddEvent(name string, options ...EventOption) {
|
|
cfg := eventConfigFromOptions(options...)
|
|
for _, tsp := range s.internalSpans {
|
|
tsp.addEvent(name, cfg)
|
|
}
|
|
}
|
|
|
|
func (s *TraceSpan) RecordError(err error, options ...EventOption) {
|
|
cfg := eventConfigFromOptions(options...)
|
|
for _, tsp := range s.internalSpans {
|
|
tsp.recordError(err, cfg)
|
|
}
|
|
}
|
|
|
|
func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}})
|
|
return s
|
|
}
|
|
|
|
func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) {
|
|
starters := t.starters
|
|
if starters == nil {
|
|
starters = globalTraceStarters
|
|
}
|
|
ts := &TraceSpan{name: spanName, startTime: time.Now()}
|
|
parentSpan := GetContextSpan(ctx)
|
|
if parentSpan != nil {
|
|
parentSpan.mu.Lock()
|
|
parentSpan.children = append(parentSpan.children, ts)
|
|
parentSpan.mu.Unlock()
|
|
ts.parent = parentSpan
|
|
}
|
|
|
|
parentCtx := ctx
|
|
for internalSpanIdx, tsp := range starters {
|
|
var internalSpan traceSpanInternal
|
|
if parentSpan != nil {
|
|
parentCtx = parentSpan.internalContexts[internalSpanIdx]
|
|
}
|
|
ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx)
|
|
ts.internalContexts = append(ts.internalContexts, ctx)
|
|
ts.internalSpans = append(ts.internalSpans, internalSpan)
|
|
}
|
|
ctx = context.WithValue(ctx, contextKeySpan, ts)
|
|
return ctx, ts
|
|
}
|
|
|
|
type mutableContext interface {
|
|
context.Context
|
|
SetContextValue(key, value any)
|
|
GetContextValue(key any) any
|
|
}
|
|
|
|
// StartInContext starts a trace span in Gitea's mutable context (usually the web request context).
|
|
// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context.
|
|
// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span".
|
|
func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) {
|
|
curTraceSpan := GetContextSpan(ctx)
|
|
_, newTraceSpan := GetTracer().Start(ctx, spanName)
|
|
ctx.SetContextValue(contextKeySpan, newTraceSpan)
|
|
return newTraceSpan, func() {
|
|
newTraceSpan.End()
|
|
ctx.SetContextValue(contextKeySpan, curTraceSpan)
|
|
}
|
|
}
|
|
|
|
func (s *TraceSpan) End() {
|
|
s.mu.Lock()
|
|
s.endTime = time.Now()
|
|
s.mu.Unlock()
|
|
|
|
for _, tsp := range s.internalSpans {
|
|
tsp.end()
|
|
}
|
|
}
|
|
|
|
func GetTracer() *Tracer {
|
|
return &Tracer{}
|
|
}
|
|
|
|
func GetContextSpan(ctx context.Context) *TraceSpan {
|
|
ts, _ := ctx.Value(contextKeySpan).(*TraceSpan)
|
|
return ts
|
|
}
|