Skip to content
Merged
47 changes: 47 additions & 0 deletions router-tests/telemetry/attribute_processor_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package telemetry

import (
"context"
"fmt"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/wundergraph/cosmo/router-tests/testenv"
"github.com/wundergraph/cosmo/router/core"
"github.com/wundergraph/cosmo/router/pkg/config"
"github.com/wundergraph/cosmo/router/pkg/trace/tracetest"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
"go.uber.org/zap/zapcore"
)
Expand Down Expand Up @@ -184,6 +188,32 @@ func TestAttributeProcessorIntegration(t *testing.T) {
})
})

t.Run("invalid UTF-8 export error logs config hint", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
TraceExporter: &invalidUTF8Exporter{},
LogObservation: testenv.LogObservationConfig{
Enabled: true,
LogLevel: zapcore.ErrorLevel,
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `query { employees { id } }`,
})
require.Equal(t, 200, res.Response.StatusCode)

require.Eventually(t, func() bool {
logs := xEnv.Observer().FilterMessageSnippet("sanitize_utf8").All()
return len(logs) > 0
}, 5*time.Second, 100*time.Millisecond)

logs := xEnv.Observer().FilterMessageSnippet("sanitize_utf8").All()
require.NotEmpty(t, logs)
require.Equal(t, logs[0].Message, "otel error: traces export: string field contains invalid UTF-8: Enable 'telemetry.tracing.sanitize_utf8.enabled' in your config to sanitize invalid UTF-8 attributes.")
})
})

t.Run("IPAnonymization hashes IP attributes", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -220,3 +250,20 @@ func TestAttributeProcessorIntegration(t *testing.T) {
})
})
}

// errInvalidUTF8 mimics google.golang.org/protobuf/internal/impl.errInvalidUTF8
// so that errors.As can match it via the invalidUTF8Error interface used in the router.
type errInvalidUTF8 struct{}

func (errInvalidUTF8) Error() string { return "string field contains invalid UTF-8" }
func (errInvalidUTF8) InvalidUTF8() bool { return true }

type invalidUTF8Exporter struct{}

func (e *invalidUTF8Exporter) ExportSpans(_ context.Context, _ []sdktrace.ReadOnlySpan) error {
return fmt.Errorf("traces export: %w", errInvalidUTF8{})
}

func (e *invalidUTF8Exporter) Shutdown(_ context.Context) error {
return nil
}
34 changes: 34 additions & 0 deletions router/pkg/trace/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package trace

import (
"errors"
"go.uber.org/zap"
)

// invalidUTF8Error matches the InvalidUTF8() method exposed by
// google.golang.org/protobuf/internal/impl.errInvalidUTF8.
type invalidUTF8Error interface {
InvalidUTF8() bool
}

// hasInvalidUTF8Error walks the error chain looking for an error
// that implements the invalidUTF8Error interface.
func hasInvalidUTF8Error(err error) bool {
var target invalidUTF8Error
if errors.As(err, &target) {
return target.InvalidUTF8()
}
return false
}

func errHandler(config *ProviderConfig) func(err error) {
return func(err error) {
if hasInvalidUTF8Error(err) {
config.Logger.Error(
"otel error: traces export: string field contains invalid UTF-8: Enable 'telemetry.tracing.sanitize_utf8.enabled' in your config to sanitize invalid UTF-8 attributes.",
zap.Error(err))
return
}
config.Logger.Error("otel error", zap.Error(err))
}
}
4 changes: 1 addition & 3 deletions router/pkg/trace/meter.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ func NewTracerProvider(ctx context.Context, config *ProviderConfig) (*sdktrace.T
otel.SetTracerProvider(tp)
}

otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
config.Logger.Error("otel error", zap.Error(err))
}))
otel.SetErrorHandler(otel.ErrorHandlerFunc(errHandler(config)))

return tp, nil
}
Loading