Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions go/core/internal/telemetry/logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package telemetry

import (
"context"
"fmt"

otelzap "go.opentelemetry.io/contrib/bridges/otelzap"
"go.opentelemetry.io/contrib/exporters/autoexport"
logglobal "go.opentelemetry.io/otel/log/global"
sdklog "go.opentelemetry.io/otel/sdk/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
crzap "sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/kagent-dev/kagent/go/core/pkg/env"
)

const loggerBridgeName = "github.com/kagent-dev/kagent/go/core"

// InitLoggerProvider configures an OTLP LoggerProvider and registers it as the
// global OTel logger provider. The exporter type and endpoint are read from the
// standard OTEL environment variables via autoexport, mirroring
// InitTracerProvider. The returned shutdown function must be called on process
// exit to flush in-flight log records. When OTEL_LOGGING_ENABLED is unset the
// pipeline is not created and a no-op shutdown is returned (default-OFF).
func InitLoggerProvider(ctx context.Context, serviceVersion string) (func(context.Context) error, error) {
if !env.OtelLoggingEnabled.Get() {
return func(context.Context) error { return nil }, nil
}

exporter, err := autoexport.NewLogExporter(ctx)
if err != nil {
return nil, fmt.Errorf("create log exporter: %w", err)
}

res, err := newTelemetryResource(ctx, serviceVersion)
if err != nil {
return nil, err
}

lp := sdklog.NewLoggerProvider(
sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
sdklog.WithResource(res),
)

logglobal.SetLoggerProvider(lp)

return lp.Shutdown, nil
}

// ControllerZapOpts returns controller-runtime zap options. When
// OTEL_LOGGING_ENABLED is set it additively tees the controller's stdout zap
// core with an otelzap bridge core, routing the controller's own logs through
// the global OTLP LoggerProvider while preserving stdout logging. When disabled
// it returns no options, leaving the logger byte-identical to upstream.
//
// InitLoggerProvider must be called first so the bridge core binds to the
// configured global LoggerProvider.
func ControllerZapOpts() []crzap.Opts {
if !env.OtelLoggingEnabled.Get() {
return nil
}
bridgeCore := otelzap.NewCore(loggerBridgeName,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this actually gets us log-trace correlation as-is, which is what you're trying to deliver?

otelzap.Core only adds trace_id/span_id to a record when a context.Context is passed as a zap field. controller-runtime logs through logr -> zapr, and log.FromContext(ctx) only threads values in via WithValues, so I am not sure these records contain spans linked to them.

otelzap.WithLoggerProvider(logglobal.GetLoggerProvider()),
)
return []crzap.Opts{
crzap.RawZapOpts(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewTee(core, bridgeCore)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OTel log SDK puts severity filtering in a processor, not in the exporter or the bridge, so someone running at error level only expects error+ logs, but when they enable this flag it will ship all their info/debug logs to the backend.

We can fix it via https://pkg.go.dev/go.opentelemetry.io/contrib/processors/minsev

})),
}
}
51 changes: 51 additions & 0 deletions go/core/internal/telemetry/logging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package telemetry

import (
"context"
"testing"

crzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
)

// TestControllerZapOpts_DisabledByDefault verifies no zap options are added when
// OTEL_LOGGING_ENABLED is off, keeping the controller logger unchanged.
func TestControllerZapOpts_DisabledByDefault(t *testing.T) {
t.Setenv("OTEL_LOGGING_ENABLED", "false")
if opts := ControllerZapOpts(); opts != nil {
t.Fatalf("expected nil opts when logging disabled, got %d", len(opts))
}
}

// TestControllerZapOpts_EnabledTeesLogger verifies that when OTEL_LOGGING_ENABLED
// is set, an otelzap bridge option is returned and the resulting logger is usable
// (the bridge tees onto the stdout core without panicking).
func TestControllerZapOpts_EnabledTeesLogger(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also test

  1. a record actually having trace_id when there's an active span, and
  2. severity gating vs the stdout level

t.Setenv("OTEL_LOGGING_ENABLED", "true")

opts := ControllerZapOpts()
if len(opts) != 1 {
t.Fatalf("expected 1 zap opt when logging enabled, got %d", len(opts))
}

// Building and using the logger must not panic even though no real OTLP
// LoggerProvider is configured (the global provider defaults to a no-op).
logger := crzap.New(append([]crzap.Opts{crzap.UseDevMode(true)}, opts...)...)
logger.Info("tee smoke test")
}

// TestInitLoggerProvider_DisabledNoop verifies the returned shutdown is a safe
// no-op when logging is disabled.
func TestInitLoggerProvider_DisabledNoop(t *testing.T) {
t.Setenv("OTEL_LOGGING_ENABLED", "false")

shutdown, err := InitLoggerProvider(context.Background(), "test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if shutdown == nil {
t.Fatal("expected non-nil shutdown")
}
if err := shutdown(context.Background()); err != nil {
t.Fatalf("no-op shutdown returned error: %v", err)
}
}
37 changes: 24 additions & 13 deletions go/core/internal/telemetry/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ func InitTracerProvider(ctx context.Context, serviceVersion string) (func(contex
return nil, fmt.Errorf("create span exporter: %w", err)
}

res, err := newTelemetryResource(ctx, serviceVersion)
if err != nil {
return nil, err
}

tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)

otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))

return tp.Shutdown, nil
}

// newTelemetryResource builds the OTel resource shared by the controller's
// signal pipelines (traces and logs), keeping their resource attributes
// identical.
func newTelemetryResource(ctx context.Context, serviceVersion string) (*resource.Resource, error) {
instanceID, err := os.Hostname()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since newTelemetryResource is now called once by InitTracerProvider and once by InitLoggerProvider, and os.Hostname() falls back to a fresh uuid.New() on failure, it's possible that processes can end up with two different service.instance.ids across the two ttelemetry signals.

Let's build the resource once in app.go and pass the same *resource.Resource into both initializers instead.

if err != nil || instanceID == "" {
instanceID = uuid.New().String()
Expand Down Expand Up @@ -67,17 +90,5 @@ func InitTracerProvider(ctx context.Context, serviceVersion string) (func(contex
if err != nil {
return nil, fmt.Errorf("create OTEL resource: %w", err)
}

tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)

otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))

return tp.Shutdown, nil
return res, nil
}
19 changes: 18 additions & 1 deletion go/core/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,24 @@ func Start(getExtensionConfig GetExtensionConfig, migrationRunner MigrationRunne
setupLog.Error(err, "failed to load configuration from environment variables")
os.Exit(1)
}
logger := zap.New(zap.UseFlagOptions(&opts))
// Initialize the OTLP logger provider before building the controller logger
// so the otelzap bridge core added by ControllerZapOpts binds to it. No-op
// unless OTEL_LOGGING_ENABLED.
shutdownLogging, err := telemetry.InitLoggerProvider(ctx, Version)
if err != nil {
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
setupLog.Error(err, "failed to initialize logging")
os.Exit(1)
}
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := shutdownLogging(shutdownCtx); err != nil {
setupLog.Error(err, "failed to shutdown logging")
Comment on lines +358 to +362
}
}()

logger := zap.New(append([]zap.Opts{zap.UseFlagOptions(&opts)}, telemetry.ControllerZapOpts()...)...)
ctrl.SetLogger(logger)

shutdownTracing, err := telemetry.InitTracerProvider(ctx, Version)
Expand Down
3 changes: 2 additions & 1 deletion go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ require (
github.com/pgvector/pgvector-go/pgx v0.4.0
github.com/testcontainers/testcontainers-go v0.43.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0
go.opentelemetry.io/contrib/bridges/otelzap v0.19.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/sdk/log v0.20.0
google.golang.org/grpc v1.81.1
k8s.io/apiextensions-apiserver v0.36.2
Expand Down Expand Up @@ -404,7 +406,6 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 // indirect
go.opentelemetry.io/otel/log v0.20.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,8 @@ go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDoj
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelzap v0.19.0 h1:48Eq3xxFx2KlL/tF7lnl42kKJBDlhNTLRzv0h154JnM=
go.opentelemetry.io/contrib/bridges/otelzap v0.19.0/go.mod h1:cQbV77F0u6HmtZPiQD9oxp2esaOEb4uLqIta6OFIKOk=
go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 h1:saQoWg5845Q8TojpqeVStS7zGwVZ6bc5W2PJavTPiBM=
go.opentelemetry.io/contrib/bridges/prometheus v0.69.0/go.mod h1:AAaS6xs5AyqMdR3Ir0nSWK+QudL2XM8Vbw5INzUxNc8=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
Expand Down Expand Up @@ -891,6 +893,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0 h1:bl2S7Ubua0Nms+D
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.44.0/go.mod h1:L0hRV50XdVIODHUfWEqGRCXQvj2rV82STVo12FMFBU0=
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
go.opentelemetry.io/otel/log/logtest v0.20.0 h1:+tsZVE15N+RWyN9lUzsRyw7hMZXNMepGu105Eim82/k=
go.opentelemetry.io/otel/log/logtest v0.20.0/go.mod h1:zS9Ryx9RrEAG2tgapMBSvacwhVSSOGSaSiWWgW3NPlQ=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
Expand Down
Loading