mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 01:04:25 +01:00 
			
		
		
		
	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
 | |
| }
 |