Skip to content

Commit 3a65611

Browse files
committed
fix
1 parent 1ea5216 commit 3a65611

File tree

9 files changed

+277
-13
lines changed

9 files changed

+277
-13
lines changed

cmd/web.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"code.gitea.io/gitea/modules/graceful"
2121
"code.gitea.io/gitea/modules/gtprof"
2222
"code.gitea.io/gitea/modules/log"
23+
"code.gitea.io/gitea/modules/otelexporter"
2324
"code.gitea.io/gitea/modules/process"
2425
"code.gitea.io/gitea/modules/public"
2526
"code.gitea.io/gitea/modules/setting"
@@ -220,7 +221,12 @@ func serveInstalled(ctx *cli.Context) error {
220221
}
221222
}
222223

223-
gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond))
224+
gtprof.EnableTracer(&gtprof.TracerOptions{
225+
ServiceName: "gitea",
226+
AppVer: setting.AppVer,
227+
BuiltinThreshold: util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond),
228+
})
229+
otelexporter.InitDefaultOtelExporter()
224230

225231
// Set up Chi routes
226232
webRoutes := routers.NormalRoutes()

custom/conf/app.example.ini

+13
Original file line numberDiff line numberDiff line change
@@ -2815,3 +2815,16 @@ LEVEL = Info
28152815
;SERVICE_TYPE = memory
28162816
;; Ignored for the "memory" type. For "redis" use something like `redis://127.0.0.1:6379/0`
28172817
;SERVICE_CONN_STR =
2818+
2819+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2820+
;; OpenTelemetry exporter
2821+
;; These are the supported options picked from https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
2822+
;; This feature is experimental and submit to change
2823+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2824+
;[otel_exporter]
2825+
;OTLP_ENABLED = false
2826+
;OTLP_ENDPOINT = http://localhost:4318
2827+
;OTLP_HEADERS =
2828+
;OTLP_TIMEOUT = 10s
2829+
;OTLP_COMPRESSION = gzip
2830+
;OTLP_TLS_INSECURE = false

modules/gtprof/gtprof.go

+16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
package gtprof
55

6+
import (
7+
"sync/atomic"
8+
"time"
9+
)
10+
611
// This is a Gitea-specific profiling package,
712
// the name is chosen to distinguish it from the standard pprof tool and "GNU gprof"
813

@@ -23,3 +28,14 @@ const LabelProcessType = "process_type"
2328

2429
// LabelProcessDescription is a label set on goroutines that have a process attached
2530
const LabelProcessDescription = "process_description"
31+
32+
type TracerOptions struct {
33+
ServiceName, AppVer string
34+
BuiltinThreshold time.Duration
35+
}
36+
37+
var tracerOptions atomic.Pointer[TracerOptions]
38+
39+
func EnableTracer(opts *TracerOptions) {
40+
tracerOptions.Store(opts)
41+
}

modules/gtprof/trace.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ package gtprof
55

66
import (
77
"context"
8+
"crypto/rand"
9+
"encoding/hex"
810
"fmt"
11+
mathRand "math/rand/v2"
912
"sync"
1013
"time"
1114

@@ -36,6 +39,7 @@ type TraceSpan struct {
3639

3740
// mutable, must be protected by mutex
3841
mu sync.RWMutex
42+
id string
3943
name string
4044
statusCode uint32
4145
statusDesc string
@@ -54,6 +58,11 @@ type TraceValue struct {
5458
v any
5559
}
5660

61+
func (t *TraceValue) IsString() bool {
62+
_, ok := t.v.(string)
63+
return ok
64+
}
65+
5766
func (t *TraceValue) AsString() string {
5867
return fmt.Sprint(t.v)
5968
}
@@ -71,6 +80,7 @@ func (t *TraceValue) AsFloat64() float64 {
7180
var globalTraceStarters []traceStarter
7281

7382
type Tracer struct {
83+
chacha8 *mathRand.ChaCha8
7484
starters []traceStarter
7585
}
7686

@@ -113,7 +123,7 @@ func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *
113123
if starters == nil {
114124
starters = globalTraceStarters
115125
}
116-
ts := &TraceSpan{name: spanName, startTime: time.Now()}
126+
ts := &TraceSpan{id: t.randomHexForBytes(8), name: spanName, startTime: time.Now()}
117127
parentSpan := GetContextSpan(ctx)
118128
if parentSpan != nil {
119129
parentSpan.mu.Lock()
@@ -165,8 +175,19 @@ func (s *TraceSpan) End() {
165175
}
166176
}
167177

178+
func (t *Tracer) randomHexForBytes(n int) string {
179+
b := make([]byte, n)
180+
_, _ = t.chacha8.Read(b) // it never fails
181+
return hex.EncodeToString(b)
182+
}
183+
168184
func GetTracer() *Tracer {
169-
return &Tracer{}
185+
var seed [32]byte
186+
_, err := rand.Read(seed[:])
187+
if err != nil {
188+
panic(fmt.Sprintf("rand.Read: %v", err))
189+
}
190+
return &Tracer{chacha8: mathRand.NewChaCha8(seed)}
170191
}
171192

172193
func GetContextSpan(ctx context.Context) *TraceSpan {

modules/gtprof/trace_builtin.go

+6-10
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"context"
88
"fmt"
99
"strings"
10-
"sync/atomic"
11-
"time"
1210

1311
"code.gitea.io/gitea/modules/tailmsg"
1412
)
@@ -72,12 +70,16 @@ func (t *traceBuiltinSpan) end() {
7270
if t.ts.parent == nil {
7371
// TODO: debug purpose only
7472
// TODO: it should distinguish between http response network lag and actual processing time
75-
threshold := time.Duration(traceBuiltinThreshold.Load())
76-
if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold {
73+
opts := tracerOptions.Load()
74+
if opts == nil {
75+
return
76+
}
77+
if t.ts.endTime.Sub(t.ts.startTime) > opts.BuiltinThreshold {
7778
sb := &strings.Builder{}
7879
t.toString(sb, 0)
7980
tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
8081
}
82+
otelRecordTrace(t)
8183
}
8284
}
8385

@@ -88,9 +90,3 @@ func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, i
8890
func init() {
8991
globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
9092
}
91-
92-
var traceBuiltinThreshold atomic.Int64
93-
94-
func EnableBuiltinTracer(threshold time.Duration) {
95-
traceBuiltinThreshold.Store(int64(threshold))
96-
}

modules/gtprof/trace_otel.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package gtprof
5+
6+
import (
7+
"strconv"
8+
9+
"code.gitea.io/gitea/modules/otelexporter"
10+
)
11+
12+
func otelToSpan(traceID string, t *traceBuiltinSpan, scopeSpan *otelexporter.OtelScopeSpan) {
13+
t.ts.mu.RLock()
14+
defer t.ts.mu.RUnlock()
15+
16+
span := &otelexporter.OtelSpan{
17+
TraceID: traceID,
18+
SpanID: t.ts.id,
19+
Name: t.ts.name,
20+
StartTimeUnixNano: strconv.FormatInt(t.ts.startTime.UnixNano(), 10),
21+
EndTimeUnixNano: strconv.FormatInt(t.ts.endTime.UnixNano(), 10),
22+
Kind: 2,
23+
}
24+
25+
if t.ts.parent != nil {
26+
span.ParentSpanID = t.ts.parent.id
27+
}
28+
29+
scopeSpan.Spans = append(scopeSpan.Spans, span)
30+
31+
for _, a := range t.ts.attributes {
32+
var otelVal any
33+
if a.Value.IsString() {
34+
otelVal = otelexporter.OtelAttributeStringValue{StringValue: a.Value.AsString()}
35+
} else {
36+
otelVal = otelexporter.OtelAttributeIntValue{IntValue: strconv.FormatInt(a.Value.AsInt64(), 10)}
37+
}
38+
span.Attributes = append(span.Attributes, &otelexporter.OtelAttribute{Key: a.Key, Value: otelVal})
39+
}
40+
41+
for _, c := range t.ts.children {
42+
child := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
43+
otelToSpan(traceID, child, scopeSpan)
44+
}
45+
}
46+
47+
func otelRecordTrace(t *traceBuiltinSpan) {
48+
exporter := otelexporter.GetDefaultOtelExporter()
49+
if exporter == nil {
50+
return
51+
}
52+
opts := tracerOptions.Load()
53+
scopeSpan := &otelexporter.OtelScopeSpan{
54+
Scope: &otelexporter.OtelScope{Name: "gitea-server", Version: opts.AppVer},
55+
}
56+
57+
traceID := GetTracer().randomHexForBytes(16)
58+
otelToSpan(traceID, t, scopeSpan)
59+
60+
resSpans := otelexporter.OtelResourceSpan{
61+
Resource: &otelexporter.OtelResource{
62+
Attributes: []*otelexporter.OtelAttribute{
63+
{Key: "service.name", Value: otelexporter.OtelAttributeStringValue{StringValue: opts.ServiceName}},
64+
},
65+
},
66+
ScopeSpans: []*otelexporter.OtelScopeSpan{scopeSpan},
67+
}
68+
69+
otelTrace := &otelexporter.OtelTrace{ResourceSpans: []*otelexporter.OtelResourceSpan{&resSpans}}
70+
exporter.ExportTrace(otelTrace)
71+
}

modules/otelexporter/otelexporter.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package otelexporter
5+
6+
import (
7+
"bytes"
8+
"net/http"
9+
"sync/atomic"
10+
11+
"code.gitea.io/gitea/modules/json"
12+
)
13+
14+
type OtelAttributeStringValue struct {
15+
StringValue string `json:"stringValue"`
16+
}
17+
18+
type OtelAttributeIntValue struct {
19+
IntValue string `json:"intValue"`
20+
}
21+
22+
type OtelAttribute struct {
23+
Key string `json:"key"`
24+
Value any `json:"value"`
25+
}
26+
27+
type OtelResource struct {
28+
Attributes []*OtelAttribute `json:"attributes,omitempty"`
29+
}
30+
31+
type OtelScope struct {
32+
Name string `json:"name"`
33+
Version string `json:"version"`
34+
Attributes []*OtelAttribute `json:"attributes,omitempty"`
35+
}
36+
37+
type OtelSpan struct {
38+
TraceID string `json:"traceId"`
39+
SpanID string `json:"spanId"`
40+
ParentSpanID string `json:"parentSpanId,omitempty"`
41+
Name string `json:"name"`
42+
StartTimeUnixNano string `json:"startTimeUnixNano"`
43+
EndTimeUnixNano string `json:"endTimeUnixNano"`
44+
Kind int `json:"kind"`
45+
Attributes []*OtelAttribute `json:"attributes,omitempty"`
46+
}
47+
48+
type OtelScopeSpan struct {
49+
Scope *OtelScope `json:"scope"`
50+
Spans []*OtelSpan `json:"spans"`
51+
}
52+
53+
type OtelResourceSpan struct {
54+
Resource *OtelResource `json:"resource"`
55+
ScopeSpans []*OtelScopeSpan `json:"scopeSpans"`
56+
}
57+
58+
type OtelTrace struct {
59+
ResourceSpans []*OtelResourceSpan `json:"resourceSpans"`
60+
}
61+
62+
type OtelExporter struct{}
63+
64+
func (e *OtelExporter) ExportTrace(trace *OtelTrace) {
65+
// TODO: use a async queue
66+
otelTraceJSON, err := json.Marshal(trace)
67+
if err == nil {
68+
_, _ = http.Post("http://localhost:4318/v1/traces", "application/json", bytes.NewReader(otelTraceJSON))
69+
}
70+
}
71+
72+
var defaultOtelExporter atomic.Pointer[OtelExporter]
73+
74+
func GetDefaultOtelExporter() *OtelExporter {
75+
return defaultOtelExporter.Load()
76+
}
77+
78+
func InitDefaultOtelExporter() {
79+
e := &OtelExporter{}
80+
defaultOtelExporter.Store(e)
81+
}

modules/setting/otel.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package setting
5+
6+
import (
7+
"fmt"
8+
"net/url"
9+
"strings"
10+
"time"
11+
)
12+
13+
type OtelExporterStruct struct {
14+
OtlpEnabled bool
15+
OtlpEndpoint string
16+
OtlpCompression string
17+
OtlpTLSInsecure bool
18+
19+
OtlpHeaders map[string]string `ini:"-"`
20+
OtlpTimeout time.Duration `ini:"-"`
21+
}
22+
23+
var OtelExporter OtelExporterStruct
24+
25+
func loadOtelExporterFrom(cfg ConfigProvider) error {
26+
OtelExporter = OtelExporterStruct{
27+
OtlpEndpoint: "http://localhost:4318",
28+
OtlpCompression: "gzip",
29+
OtlpTimeout: time.Second * 10,
30+
}
31+
sec := cfg.Section("otel_exporter")
32+
if err := sec.MapTo(&OtelExporter); err != nil {
33+
return err
34+
}
35+
if !OtelExporter.OtlpEnabled {
36+
return nil
37+
}
38+
39+
OtelExporter.OtlpTimeout = sec.Key("OTLP_TIMEOUT").MustDuration(OtelExporter.OtlpTimeout)
40+
41+
otlpHeadersString := sec.Key("OTLP_HEADERS").String()
42+
if otlpHeadersString != "" {
43+
OtelExporter.OtlpHeaders = make(map[string]string)
44+
for _, header := range strings.Split(otlpHeadersString, ",") {
45+
header = strings.TrimSpace(header)
46+
if key, valRaw, ok := strings.Cut(header, "="); ok {
47+
val, err := url.QueryUnescape(valRaw)
48+
if err != nil {
49+
return fmt.Errorf("invalid OTLP_HEADER %q, err: %w", header, err)
50+
}
51+
OtelExporter.OtlpHeaders[key] = val
52+
}
53+
}
54+
}
55+
56+
return nil
57+
}

modules/setting/setting.go

+3
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ func LoadSettings() {
217217
loadProjectFrom(CfgProvider)
218218
loadMimeTypeMapFrom(CfgProvider)
219219
loadFederationFrom(CfgProvider)
220+
if err := loadOtelExporterFrom(CfgProvider); err != nil {
221+
log.Fatal("Unable to load otel_exporter settings: %v", err)
222+
}
220223
}
221224

222225
// LoadSettingsForInstall initializes the settings for install

0 commit comments

Comments
 (0)