mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
4.0 KiB
175 lines
4.0 KiB
// 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 |
|
}
|
|
|