// 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 }