c738542201
This PR adds opentelemetry and chi wrapper to have basic instrumentation <!--start release-notes-assistant--> ## Draft release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/3972): <!--number 3972 --><!--line 0 --><!--description YWRkIHN1cHBvcnQgZm9yIGJhc2ljIHJlcXVlc3QgdHJhY2luZyB3aXRoIG9wZW50ZWxlbWV0cnk=-->add support for basic request tracing with opentelemetry<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3972 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
199 lines
5.6 KiB
Go
199 lines
5.6 KiB
Go
// Copyright 2024 TheFox0x7. All rights reserved.
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package setting
|
|
|
|
import (
|
|
"net/url"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
)
|
|
|
|
const (
|
|
opentelemetrySectionName string = "opentelemetry"
|
|
exporter string = ".exporter"
|
|
otlp string = ".otlp"
|
|
alwaysOn string = "always_on"
|
|
alwaysOff string = "always_off"
|
|
traceIDRatio string = "traceidratio"
|
|
parentBasedAlwaysOn string = "parentbased_always_on"
|
|
parentBasedAlwaysOff string = "parentbased_always_off"
|
|
parentBasedTraceIDRatio string = "parentbased_traceidratio"
|
|
)
|
|
|
|
var OpenTelemetry = struct {
|
|
// Inverse of OTEL_SDK_DISABLE, skips telemetry setup
|
|
Enabled bool
|
|
ServiceName string
|
|
ResourceAttributes string
|
|
ResourceDetectors string
|
|
Sampler sdktrace.Sampler
|
|
Traces string
|
|
|
|
OtelTraces *OtelExporter
|
|
}{
|
|
ServiceName: "forgejo",
|
|
Traces: "otel",
|
|
}
|
|
|
|
type OtelExporter struct {
|
|
Endpoint *url.URL `ini:"ENDPOINT"`
|
|
Headers map[string]string `ini:"-"`
|
|
Compression string `ini:"COMPRESSION"`
|
|
Certificate string `ini:"CERTIFICATE"`
|
|
ClientKey string `ini:"CLIENT_KEY"`
|
|
ClientCertificate string `ini:"CLIENT_CERTIFICATE"`
|
|
Timeout time.Duration `ini:"TIMEOUT"`
|
|
Protocol string `ini:"-"`
|
|
}
|
|
|
|
func createOtlpExporterConfig(rootCfg ConfigProvider, section string) *OtelExporter {
|
|
protocols := []string{"http/protobuf", "grpc"}
|
|
endpoint, _ := url.Parse("http://localhost:4318/")
|
|
exp := &OtelExporter{
|
|
Endpoint: endpoint,
|
|
Timeout: 10 * time.Second,
|
|
Headers: map[string]string{},
|
|
Protocol: "http/protobuf",
|
|
}
|
|
|
|
loadSection := func(name string) {
|
|
otlp := rootCfg.Section(name)
|
|
if otlp.HasKey("ENDPOINT") {
|
|
endpoint, err := url.Parse(otlp.Key("ENDPOINT").String())
|
|
if err != nil {
|
|
log.Warn("Endpoint parsing failed, section: %s, err %v", name, err)
|
|
} else {
|
|
exp.Endpoint = endpoint
|
|
}
|
|
}
|
|
if err := otlp.MapTo(exp); err != nil {
|
|
log.Warn("Mapping otlp settings failed, section: %s, err: %v", name, err)
|
|
}
|
|
|
|
exp.Protocol = otlp.Key("PROTOCOL").In(exp.Protocol, protocols)
|
|
|
|
headers := otlp.Key("HEADERS").String()
|
|
if headers != "" {
|
|
for k, v := range _stringToHeader(headers) {
|
|
exp.Headers[k] = v
|
|
}
|
|
}
|
|
}
|
|
loadSection("opentelemetry.exporter.otlp")
|
|
|
|
loadSection("opentelemetry.exporter.otlp" + section)
|
|
|
|
if len(exp.Certificate) > 0 && !filepath.IsAbs(exp.Certificate) {
|
|
exp.Certificate = filepath.Join(CustomPath, exp.Certificate)
|
|
}
|
|
if len(exp.ClientCertificate) > 0 && !filepath.IsAbs(exp.ClientCertificate) {
|
|
exp.ClientCertificate = filepath.Join(CustomPath, exp.ClientCertificate)
|
|
}
|
|
if len(exp.ClientKey) > 0 && !filepath.IsAbs(exp.ClientKey) {
|
|
exp.ClientKey = filepath.Join(CustomPath, exp.ClientKey)
|
|
}
|
|
|
|
return exp
|
|
}
|
|
|
|
func loadOpenTelemetryFrom(rootCfg ConfigProvider) {
|
|
sec := rootCfg.Section(opentelemetrySectionName)
|
|
OpenTelemetry.Enabled = sec.Key("ENABLED").MustBool(false)
|
|
if !OpenTelemetry.Enabled {
|
|
return
|
|
}
|
|
|
|
// Load resource related settings
|
|
OpenTelemetry.ServiceName = sec.Key("SERVICE_NAME").MustString("forgejo")
|
|
OpenTelemetry.ResourceAttributes = sec.Key("RESOURCE_ATTRIBUTES").String()
|
|
OpenTelemetry.ResourceDetectors = strings.ToLower(sec.Key("RESOURCE_DETECTORS").String())
|
|
|
|
// Load tracing related settings
|
|
samplers := make([]string, 0, len(sampler))
|
|
for k := range sampler {
|
|
samplers = append(samplers, k)
|
|
}
|
|
|
|
samplerName := sec.Key("TRACES_SAMPLER").In(parentBasedAlwaysOn, samplers)
|
|
samplerArg := sec.Key("TRACES_SAMPLER_ARG").MustString("")
|
|
OpenTelemetry.Sampler = sampler[samplerName](samplerArg)
|
|
|
|
switch sec.Key("TRACES_EXPORTER").MustString("otlp") {
|
|
case "none":
|
|
OpenTelemetry.Traces = "none"
|
|
default:
|
|
OpenTelemetry.Traces = "otlp"
|
|
OpenTelemetry.OtelTraces = createOtlpExporterConfig(rootCfg, ".traces")
|
|
}
|
|
}
|
|
|
|
var sampler = map[string]func(arg string) sdktrace.Sampler{
|
|
alwaysOff: func(_ string) sdktrace.Sampler {
|
|
return sdktrace.NeverSample()
|
|
},
|
|
alwaysOn: func(_ string) sdktrace.Sampler {
|
|
return sdktrace.AlwaysSample()
|
|
},
|
|
traceIDRatio: func(arg string) sdktrace.Sampler {
|
|
ratio, err := strconv.ParseFloat(arg, 64)
|
|
if err != nil {
|
|
ratio = 1
|
|
}
|
|
return sdktrace.TraceIDRatioBased(ratio)
|
|
},
|
|
parentBasedAlwaysOff: func(_ string) sdktrace.Sampler {
|
|
return sdktrace.ParentBased(sdktrace.NeverSample())
|
|
},
|
|
parentBasedAlwaysOn: func(_ string) sdktrace.Sampler {
|
|
return sdktrace.ParentBased(sdktrace.AlwaysSample())
|
|
},
|
|
parentBasedTraceIDRatio: func(arg string) sdktrace.Sampler {
|
|
ratio, err := strconv.ParseFloat(arg, 64)
|
|
if err != nil {
|
|
ratio = 1
|
|
}
|
|
return sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))
|
|
},
|
|
}
|
|
|
|
// Opentelemetry SDK function port
|
|
|
|
func _stringToHeader(value string) map[string]string {
|
|
headersPairs := strings.Split(value, ",")
|
|
headers := make(map[string]string)
|
|
|
|
for _, header := range headersPairs {
|
|
n, v, found := strings.Cut(header, "=")
|
|
if !found {
|
|
log.Warn("Otel header ignored on %q: missing '='", header)
|
|
continue
|
|
}
|
|
name, err := url.PathUnescape(n)
|
|
if err != nil {
|
|
log.Warn("Otel header ignored on %q, invalid header key: %s", header, n)
|
|
continue
|
|
}
|
|
trimmedName := strings.TrimSpace(name)
|
|
value, err := url.PathUnescape(v)
|
|
if err != nil {
|
|
log.Warn("Otel header ignored on %q, invalid header value: %s", header, v)
|
|
continue
|
|
}
|
|
trimmedValue := strings.TrimSpace(value)
|
|
|
|
headers[trimmedName] = trimmedValue
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
func IsOpenTelemetryEnabled() bool {
|
|
return OpenTelemetry.Enabled
|
|
}
|