forked from kevadesu/forgejo
Open telemetry integration (#3972)
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>
This commit is contained in:
parent
7c74def6ff
commit
c738542201
19 changed files with 1281 additions and 10 deletions
96
modules/opentelemetry/otel.go
Normal file
96
modules/opentelemetry/otel.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-logr/logr/funcr"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
// Redirect otel logger to write to common forgejo log at info
|
||||
logWrap := funcr.New(func(prefix, args string) {
|
||||
log.Info(fmt.Sprint(prefix, args))
|
||||
}, funcr.Options{})
|
||||
otel.SetLogger(logWrap)
|
||||
// Redirect error handling to forgejo log as well
|
||||
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) {
|
||||
log.Error("internal opentelemetry error was raised: %s", cause)
|
||||
}))
|
||||
var shutdownFuncs []func(context.Context) error
|
||||
shutdownCtx := context.Background()
|
||||
|
||||
otel.SetTextMapPropagator(newPropagator())
|
||||
|
||||
res, err := newResource(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
traceShutdown, err := setupTraceProvider(ctx, res)
|
||||
if err != nil {
|
||||
log.Warn("OpenTelemetry trace setup failed, err=%s", err)
|
||||
} else {
|
||||
shutdownFuncs = append(shutdownFuncs, traceShutdown)
|
||||
}
|
||||
|
||||
graceful.GetManager().RunAtShutdown(ctx, func() {
|
||||
for _, fn := range shutdownFuncs {
|
||||
if err := fn(shutdownCtx); err != nil {
|
||||
log.Warn("exporter shutdown failed, err=%s", err)
|
||||
}
|
||||
}
|
||||
shutdownFuncs = nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPropagator() propagation.TextMapPropagator {
|
||||
return propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
)
|
||||
}
|
||||
|
||||
func withCertPool(path string, tlsConf *tls.Config) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warn("Otel: reading ca cert failed path=%s, err=%s", path, err)
|
||||
return
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
if ok := cp.AppendCertsFromPEM(b); !ok {
|
||||
log.Warn("Otel: no valid PEM certificate found path=%s", path)
|
||||
return
|
||||
}
|
||||
tlsConf.RootCAs = cp
|
||||
}
|
||||
|
||||
func withClientCert(nc, nk string, tlsConf *tls.Config) {
|
||||
if nc == "" || nk == "" {
|
||||
return
|
||||
}
|
||||
|
||||
crt, err := tls.LoadX509KeyPair(nc, nk)
|
||||
if err != nil {
|
||||
log.Warn("Otel: create tls client key pair failed")
|
||||
return
|
||||
}
|
||||
|
||||
tlsConf.Certificates = append(tlsConf.Certificates, crt)
|
||||
}
|
121
modules/opentelemetry/otel_test.go
Normal file
121
modules/opentelemetry/otel_test.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
)
|
||||
|
||||
func TestNoopDefault(t *testing.T) {
|
||||
inMem := tracetest.NewInMemoryExporter()
|
||||
called := false
|
||||
exp := func(ctx context.Context) (sdktrace.SpanExporter, error) {
|
||||
called = true
|
||||
return inMem, nil
|
||||
}
|
||||
exporter["inmemory"] = exp
|
||||
t.Cleanup(func() {
|
||||
delete(exporter, "inmemory")
|
||||
})
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.Traces, "inmemory")
|
||||
|
||||
ctx := context.Background()
|
||||
require.NoError(t, Init(ctx))
|
||||
tracer := otel.Tracer("test_noop")
|
||||
|
||||
_, span := tracer.Start(ctx, "test span")
|
||||
defer span.End()
|
||||
|
||||
assert.False(t, span.SpanContext().HasTraceID())
|
||||
assert.False(t, span.SpanContext().HasSpanID())
|
||||
assert.False(t, called)
|
||||
}
|
||||
|
||||
func generateTestTLS(t *testing.T, path, host string) *tls.Config {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err, "Failed to generate private key: %v", err)
|
||||
|
||||
keyUsage := x509.KeyUsageDigitalSignature
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Hour)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
require.NoError(t, err, "Failed to generate serial number: %v", err)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Forgejo Testing"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: keyUsage,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
hosts := strings.Split(host, ",")
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, h)
|
||||
}
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
|
||||
require.NoError(t, err, "Failed to create certificate: %v", err)
|
||||
|
||||
certOut, err := os.Create(path + "/cert.pem")
|
||||
require.NoError(t, err, "Failed to open cert.pem for writing: %v", err)
|
||||
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
t.Fatalf("Failed to write data to cert.pem: %v", err)
|
||||
}
|
||||
if err := certOut.Close(); err != nil {
|
||||
t.Fatalf("Error closing cert.pem: %v", err)
|
||||
}
|
||||
keyOut, err := os.OpenFile(path+"/key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
require.NoError(t, err, "Failed to open key.pem for writing: %v", err)
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
require.NoError(t, err, "Unable to marshal private key: %v", err)
|
||||
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||
t.Fatalf("Failed to write data to key.pem: %v", err)
|
||||
}
|
||||
if err := keyOut.Close(); err != nil {
|
||||
t.Fatalf("Error closing key.pem: %v", err)
|
||||
}
|
||||
serverCert, err := tls.LoadX509KeyPair(path+"/cert.pem", path+"/key.pem")
|
||||
require.NoError(t, err, "failed to load the key pair")
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
ClientAuth: tls.RequireAnyClientCert,
|
||||
}
|
||||
}
|
90
modules/opentelemetry/resource.go
Normal file
90
modules/opentelemetry/resource.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
|
||||
)
|
||||
|
||||
const (
|
||||
decoderTelemetrySdk = "sdk"
|
||||
decoderProcess = "process"
|
||||
decoderOS = "os"
|
||||
decoderHost = "host"
|
||||
)
|
||||
|
||||
func newResource(ctx context.Context) (*resource.Resource, error) {
|
||||
opts := []resource.Option{
|
||||
resource.WithAttributes(parseSettingAttributes(setting.OpenTelemetry.ResourceAttributes)...),
|
||||
}
|
||||
opts = append(opts, parseDecoderOpts()...)
|
||||
opts = append(opts, resource.WithAttributes(
|
||||
semconv.ServiceName(setting.OpenTelemetry.ServiceName),
|
||||
semconv.ServiceVersion(setting.ForgejoVersion),
|
||||
))
|
||||
return resource.New(ctx, opts...)
|
||||
}
|
||||
|
||||
func parseDecoderOpts() []resource.Option {
|
||||
var opts []resource.Option
|
||||
for _, v := range strings.Split(setting.OpenTelemetry.ResourceDetectors, ",") {
|
||||
switch v {
|
||||
case decoderTelemetrySdk:
|
||||
opts = append(opts, resource.WithTelemetrySDK())
|
||||
case decoderProcess:
|
||||
opts = append(opts, resource.WithProcess())
|
||||
case decoderOS:
|
||||
opts = append(opts, resource.WithOS())
|
||||
case decoderHost:
|
||||
opts = append(opts, resource.WithHost())
|
||||
case "": // Don't warn on empty string
|
||||
default:
|
||||
log.Warn("Ignoring unknown resource decoder option: %s", v)
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func parseSettingAttributes(s string) []attribute.KeyValue {
|
||||
var attrs []attribute.KeyValue
|
||||
rawAttrs := strings.TrimSpace(s)
|
||||
|
||||
if rawAttrs == "" {
|
||||
return attrs
|
||||
}
|
||||
|
||||
pairs := strings.Split(rawAttrs, ",")
|
||||
|
||||
var invalid []string
|
||||
for _, p := range pairs {
|
||||
k, v, found := strings.Cut(p, "=")
|
||||
if !found {
|
||||
invalid = append(invalid, p)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(k)
|
||||
val, err := url.PathUnescape(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
// Retain original value if decoding fails, otherwise it will be
|
||||
// an empty string.
|
||||
val = v
|
||||
log.Warn("Otel resource attribute decoding error, retaining unescaped value. key=%s, val=%s", key, val)
|
||||
}
|
||||
attrs = append(attrs, attribute.String(key, val))
|
||||
}
|
||||
if len(invalid) > 0 {
|
||||
log.Warn("Partial resource, missing values: %v", invalid)
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
73
modules/opentelemetry/resource_test.go
Normal file
73
modules/opentelemetry/resource_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
|
||||
)
|
||||
|
||||
func TestResourceServiceName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
resource, err := newResource(ctx)
|
||||
require.NoError(t, err)
|
||||
serviceKeyIdx := slices.IndexFunc(resource.Attributes(), func(v attribute.KeyValue) bool {
|
||||
return v.Key == semconv.ServiceNameKey
|
||||
})
|
||||
require.NotEqual(t, -1, serviceKeyIdx)
|
||||
|
||||
assert.Equal(t, "forgejo", resource.Attributes()[serviceKeyIdx].Value.AsString())
|
||||
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ServiceName, "non-default value")()
|
||||
resource, err = newResource(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
serviceKeyIdx = slices.IndexFunc(resource.Attributes(), func(v attribute.KeyValue) bool {
|
||||
return v.Key == semconv.ServiceNameKey
|
||||
})
|
||||
require.NotEqual(t, -1, serviceKeyIdx)
|
||||
|
||||
assert.Equal(t, "non-default value", resource.Attributes()[serviceKeyIdx].Value.AsString())
|
||||
}
|
||||
|
||||
func TestResourceAttributes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ResourceDetectors, "foo")()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ResourceAttributes, "Test=LABEL,broken,unescape=%XXlabel")()
|
||||
res, err := newResource(ctx)
|
||||
require.NoError(t, err)
|
||||
expected, err := resource.New(ctx, resource.WithAttributes(
|
||||
semconv.ServiceName(setting.OpenTelemetry.ServiceName),
|
||||
semconv.ServiceVersion(setting.ForgejoVersion),
|
||||
attribute.String("Test", "LABEL"),
|
||||
attribute.String("unescape", "%XXlabel"),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, res)
|
||||
}
|
||||
|
||||
func TestDecoderParity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ResourceDetectors, "sdk,process,os,host")()
|
||||
exp, err := resource.New(
|
||||
ctx, resource.WithTelemetrySDK(), resource.WithOS(), resource.WithProcess(), resource.WithHost(), resource.WithAttributes(
|
||||
semconv.ServiceName(setting.OpenTelemetry.ServiceName), semconv.ServiceVersion(setting.ForgejoVersion),
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
res2, err := newResource(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exp, res2)
|
||||
}
|
98
modules/opentelemetry/traces.go
Normal file
98
modules/opentelemetry/traces.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func newGrpcExporter(ctx context.Context) (sdktrace.SpanExporter, error) {
|
||||
endpoint := setting.OpenTelemetry.OtelTraces.Endpoint
|
||||
|
||||
opts := []otlptracegrpc.Option{}
|
||||
|
||||
tlsConf := &tls.Config{}
|
||||
opts = append(opts, otlptracegrpc.WithEndpoint(endpoint.Host))
|
||||
opts = append(opts, otlptracegrpc.WithTimeout(setting.OpenTelemetry.OtelTraces.Timeout))
|
||||
switch setting.OpenTelemetry.OtelTraces.Endpoint.Scheme {
|
||||
case "http", "unix":
|
||||
opts = append(opts, otlptracegrpc.WithInsecure())
|
||||
}
|
||||
|
||||
if setting.OpenTelemetry.OtelTraces.Compression != "" {
|
||||
opts = append(opts, otlptracegrpc.WithCompressor(setting.OpenTelemetry.OtelTraces.Compression))
|
||||
}
|
||||
withCertPool(setting.OpenTelemetry.OtelTraces.Certificate, tlsConf)
|
||||
withClientCert(setting.OpenTelemetry.OtelTraces.ClientCertificate, setting.OpenTelemetry.OtelTraces.ClientKey, tlsConf)
|
||||
if tlsConf.RootCAs != nil || len(tlsConf.Certificates) > 0 {
|
||||
opts = append(opts, otlptracegrpc.WithTLSCredentials(
|
||||
credentials.NewTLS(tlsConf),
|
||||
))
|
||||
}
|
||||
opts = append(opts, otlptracegrpc.WithHeaders(setting.OpenTelemetry.OtelTraces.Headers))
|
||||
|
||||
return otlptracegrpc.New(ctx, opts...)
|
||||
}
|
||||
|
||||
func newHTTPExporter(ctx context.Context) (sdktrace.SpanExporter, error) {
|
||||
endpoint := setting.OpenTelemetry.OtelTraces.Endpoint
|
||||
opts := []otlptracehttp.Option{}
|
||||
tlsConf := &tls.Config{}
|
||||
opts = append(opts, otlptracehttp.WithEndpoint(endpoint.Host))
|
||||
switch setting.OpenTelemetry.OtelTraces.Endpoint.Scheme {
|
||||
case "http", "unix":
|
||||
opts = append(opts, otlptracehttp.WithInsecure())
|
||||
}
|
||||
switch setting.OpenTelemetry.OtelTraces.Compression {
|
||||
case "gzip":
|
||||
opts = append(opts, otlptracehttp.WithCompression(otlptracehttp.GzipCompression))
|
||||
default:
|
||||
opts = append(opts, otlptracehttp.WithCompression(otlptracehttp.NoCompression))
|
||||
}
|
||||
withCertPool(setting.OpenTelemetry.OtelTraces.Certificate, tlsConf)
|
||||
withClientCert(setting.OpenTelemetry.OtelTraces.ClientCertificate, setting.OpenTelemetry.OtelTraces.ClientKey, tlsConf)
|
||||
if tlsConf.RootCAs != nil || len(tlsConf.Certificates) > 0 {
|
||||
opts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConf))
|
||||
}
|
||||
opts = append(opts, otlptracehttp.WithHeaders(setting.OpenTelemetry.OtelTraces.Headers))
|
||||
|
||||
return otlptracehttp.New(ctx, opts...)
|
||||
}
|
||||
|
||||
var exporter = map[string]func(context.Context) (sdktrace.SpanExporter, error){
|
||||
"http/protobuf": newHTTPExporter,
|
||||
"grpc": newGrpcExporter,
|
||||
}
|
||||
|
||||
// Create new and register trace provider from user defined configuration
|
||||
func setupTraceProvider(ctx context.Context, r *resource.Resource) (func(context.Context) error, error) {
|
||||
var shutdown func(context.Context) error
|
||||
switch setting.OpenTelemetry.Traces {
|
||||
case "otlp":
|
||||
traceExporter, err := exporter[setting.OpenTelemetry.OtelTraces.Protocol](ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(setting.OpenTelemetry.Sampler),
|
||||
sdktrace.WithBatcher(traceExporter),
|
||||
sdktrace.WithResource(r),
|
||||
)
|
||||
otel.SetTracerProvider(traceProvider)
|
||||
shutdown = traceProvider.Shutdown
|
||||
default:
|
||||
shutdown = func(ctx context.Context) error { return nil }
|
||||
}
|
||||
return shutdown, nil
|
||||
}
|
114
modules/opentelemetry/traces_test.go
Normal file
114
modules/opentelemetry/traces_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2024 TheFox0x7. All rights reserved.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
func TestTraceGrpcExporter(t *testing.T) {
|
||||
grpcMethods := make(chan string)
|
||||
tlsConfig := generateTestTLS(t, os.TempDir(), "localhost,127.0.0.1")
|
||||
assert.NotNil(t, tlsConfig)
|
||||
|
||||
collector := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)), grpc.UnknownServiceHandler(func(srv any, stream grpc.ServerStream) error {
|
||||
method, _ := grpc.Method(stream.Context())
|
||||
grpcMethods <- method
|
||||
return nil
|
||||
}))
|
||||
defer collector.GracefulStop()
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
defer ln.Close()
|
||||
go collector.Serve(ln)
|
||||
|
||||
traceEndpoint, err := url.Parse("https://" + ln.Addr().String())
|
||||
require.NoError(t, err)
|
||||
config := &setting.OtelExporter{
|
||||
Endpoint: traceEndpoint,
|
||||
Certificate: os.TempDir() + "/cert.pem",
|
||||
ClientCertificate: os.TempDir() + "/cert.pem",
|
||||
ClientKey: os.TempDir() + "/key.pem",
|
||||
Protocol: "grpc",
|
||||
}
|
||||
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ServiceName, "forgejo-certs")()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.Traces, "otlp")()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.OtelTraces, config)()
|
||||
ctx := context.Background()
|
||||
require.NoError(t, Init(ctx))
|
||||
|
||||
tracer := otel.Tracer("test_tls")
|
||||
_, span := tracer.Start(ctx, "test span")
|
||||
assert.True(t, span.SpanContext().HasTraceID())
|
||||
assert.True(t, span.SpanContext().HasSpanID())
|
||||
|
||||
span.End()
|
||||
// Give the exporter time to send the span
|
||||
select {
|
||||
case method := <-grpcMethods:
|
||||
assert.Equal(t, "/opentelemetry.proto.collector.trace.v1.TraceService/Export", method)
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("no grpc call within 10s")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceHttpExporter(t *testing.T) {
|
||||
httpCalls := make(chan string)
|
||||
tlsConfig := generateTestTLS(t, os.TempDir(), "localhost,127.0.0.1")
|
||||
assert.NotNil(t, tlsConfig)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
httpCalls <- r.URL.Path
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"success": true}`))
|
||||
}))
|
||||
server.TLS = tlsConfig
|
||||
|
||||
traceEndpoint, err := url.Parse("http://" + server.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
config := &setting.OtelExporter{
|
||||
Endpoint: traceEndpoint,
|
||||
Certificate: os.TempDir() + "/cert.pem",
|
||||
ClientCertificate: os.TempDir() + "/cert.pem",
|
||||
ClientKey: os.TempDir() + "/key.pem",
|
||||
Protocol: "http/protobuf",
|
||||
}
|
||||
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.ServiceName, "forgejo-certs")()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.Traces, "otlp")()
|
||||
defer test.MockVariableValue(&setting.OpenTelemetry.OtelTraces, config)()
|
||||
ctx := context.Background()
|
||||
require.NoError(t, Init(ctx))
|
||||
|
||||
tracer := otel.Tracer("test_tls")
|
||||
_, span := tracer.Start(ctx, "test span")
|
||||
assert.True(t, span.SpanContext().HasTraceID())
|
||||
assert.True(t, span.SpanContext().HasSpanID())
|
||||
|
||||
span.End()
|
||||
select {
|
||||
case path := <-httpCalls:
|
||||
assert.Equal(t, "/v1/traces", path)
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("no http call within 10s")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue