aboutsummaryrefslogtreecommitdiffstats
path: root/modules/gtprof
diff options
context:
space:
mode:
Diffstat (limited to 'modules/gtprof')
-rw-r--r--modules/gtprof/event.go32
-rw-r--r--modules/gtprof/trace.go175
-rw-r--r--modules/gtprof/trace_builtin.go96
-rw-r--r--modules/gtprof/trace_const.go19
-rw-r--r--modules/gtprof/trace_test.go93
5 files changed, 415 insertions, 0 deletions
diff --git a/modules/gtprof/event.go b/modules/gtprof/event.go
new file mode 100644
index 0000000000..da4a0faff9
--- /dev/null
+++ b/modules/gtprof/event.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+type EventConfig struct {
+ attributes []*TraceAttribute
+}
+
+type EventOption interface {
+ applyEvent(*EventConfig)
+}
+
+type applyEventFunc func(*EventConfig)
+
+func (f applyEventFunc) applyEvent(cfg *EventConfig) {
+ f(cfg)
+}
+
+func WithAttributes(attrs ...*TraceAttribute) EventOption {
+ return applyEventFunc(func(cfg *EventConfig) {
+ cfg.attributes = append(cfg.attributes, attrs...)
+ })
+}
+
+func eventConfigFromOptions(options ...EventOption) *EventConfig {
+ cfg := &EventConfig{}
+ for _, opt := range options {
+ opt.applyEvent(cfg)
+ }
+ return cfg
+}
diff --git a/modules/gtprof/trace.go b/modules/gtprof/trace.go
new file mode 100644
index 0000000000..ad67c226dc
--- /dev/null
+++ b/modules/gtprof/trace.go
@@ -0,0 +1,175 @@
+// 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
+}
diff --git a/modules/gtprof/trace_builtin.go b/modules/gtprof/trace_builtin.go
new file mode 100644
index 0000000000..2590ed3a13
--- /dev/null
+++ b/modules/gtprof/trace_builtin.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "code.gitea.io/gitea/modules/tailmsg"
+)
+
+type traceBuiltinStarter struct{}
+
+type traceBuiltinSpan struct {
+ ts *TraceSpan
+
+ internalSpanIdx int
+}
+
+func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) {
+ // No-op because builtin tracer doesn't need it.
+ // In the future we might use it to mark the time point between backend logic and network response.
+}
+
+func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) {
+ // No-op because builtin tracer doesn't need it.
+ // Actually Gitea doesn't handle err this way in most cases
+}
+
+func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) {
+ t.ts.mu.RLock()
+ defer t.ts.mu.RUnlock()
+
+ out.WriteString(strings.Repeat(" ", indent))
+ out.WriteString(t.ts.name)
+ if t.ts.endTime.IsZero() {
+ out.WriteString(" duration: (not ended)")
+ } else {
+ fmt.Fprintf(out, " duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds())
+ }
+ for _, a := range t.ts.attributes {
+ out.WriteString(" ")
+ out.WriteString(a.Key)
+ out.WriteString("=")
+ value := a.Value.AsString()
+ if strings.ContainsAny(value, " \t\r\n") {
+ quoted := false
+ for _, c := range "\"'`" {
+ if quoted = !strings.Contains(value, string(c)); quoted {
+ value = string(c) + value + string(c)
+ break
+ }
+ }
+ if !quoted {
+ value = fmt.Sprintf("%q", value)
+ }
+ }
+ out.WriteString(value)
+ }
+ out.WriteString("\n")
+ for _, c := range t.ts.children {
+ span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
+ span.toString(out, indent+2)
+ }
+}
+
+func (t *traceBuiltinSpan) end() {
+ if t.ts.parent == nil {
+ // TODO: debug purpose only
+ // TODO: it should distinguish between http response network lag and actual processing time
+ threshold := time.Duration(traceBuiltinThreshold.Load())
+ if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold {
+ sb := &strings.Builder{}
+ t.toString(sb, 0)
+ tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
+ }
+ }
+}
+
+func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
+ return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx}
+}
+
+func init() {
+ globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
+}
+
+var traceBuiltinThreshold atomic.Int64
+
+func EnableBuiltinTracer(threshold time.Duration) {
+ traceBuiltinThreshold.Store(int64(threshold))
+}
diff --git a/modules/gtprof/trace_const.go b/modules/gtprof/trace_const.go
new file mode 100644
index 0000000000..af9ce9223f
--- /dev/null
+++ b/modules/gtprof/trace_const.go
@@ -0,0 +1,19 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
+
+const (
+ TraceSpanHTTP = "http"
+ TraceSpanGitRun = "git-run"
+ TraceSpanDatabase = "database"
+)
+
+const (
+ TraceAttrFuncCaller = "func.caller"
+ TraceAttrDbSQL = "db.sql"
+ TraceAttrGitCommand = "git.command"
+ TraceAttrHTTPRoute = "http.route"
+)
diff --git a/modules/gtprof/trace_test.go b/modules/gtprof/trace_test.go
new file mode 100644
index 0000000000..0f4e3facba
--- /dev/null
+++ b/modules/gtprof/trace_test.go
@@ -0,0 +1,93 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gtprof
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// "vendor span" is a simple demo for a span from a vendor library
+
+var vendorContextKey any = "vendorContextKey"
+
+type vendorSpan struct {
+ name string
+ children []*vendorSpan
+}
+
+func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) {
+ span := &vendorSpan{name: name}
+ parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan)
+ if ok {
+ parentSpan.children = append(parentSpan.children, span)
+ }
+ ctx = context.WithValue(ctx, vendorContextKey, span)
+ return ctx, span
+}
+
+// below "testTrace*" integrate the vendor span into our trace system
+
+type testTraceSpan struct {
+ vendorSpan *vendorSpan
+}
+
+func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {}
+
+func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {}
+
+func (t *testTraceSpan) end() {}
+
+type testTraceStarter struct{}
+
+func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
+ ctx, span := vendorTraceStart(ctx, traceSpan.name)
+ return ctx, &testTraceSpan{span}
+}
+
+func TestTraceStarter(t *testing.T) {
+ globalTraceStarters = []traceStarter{&testTraceStarter{}}
+
+ ctx := t.Context()
+ ctx, span := GetTracer().Start(ctx, "root")
+ defer span.End()
+
+ func(ctx context.Context) {
+ ctx, span := GetTracer().Start(ctx, "span1")
+ defer span.End()
+ func(ctx context.Context) {
+ _, span := GetTracer().Start(ctx, "spanA")
+ defer span.End()
+ }(ctx)
+ func(ctx context.Context) {
+ _, span := GetTracer().Start(ctx, "spanB")
+ defer span.End()
+ }(ctx)
+ }(ctx)
+
+ func(ctx context.Context) {
+ _, span := GetTracer().Start(ctx, "span2")
+ defer span.End()
+ }(ctx)
+
+ var spanFullNames []string
+ var collectSpanNames func(parentFullName string, s *vendorSpan)
+ collectSpanNames = func(parentFullName string, s *vendorSpan) {
+ fullName := parentFullName + "/" + s.name
+ spanFullNames = append(spanFullNames, fullName)
+ for _, c := range s.children {
+ collectSpanNames(fullName, c)
+ }
+ }
+ collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan)
+ assert.Equal(t, []string{
+ "/root",
+ "/root/span1",
+ "/root/span1/spanA",
+ "/root/span1/spanB",
+ "/root/span2",
+ }, spanFullNames)
+}