Skip to content
Merged
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
39 changes: 39 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package config

import (
"net"
"os"
"strings"
)

// Config is the configuration for the Coldbrew server
// It is populated from environment variables and has sensible defaults for all fields so that you can just use it as is without any configuration
// The following environment variables are supported and can be used to override the defaults for the fields
Expand Down Expand Up @@ -106,6 +112,14 @@ type Config struct {
// DisableProtoValidate disables the protovalidate interceptor in the default
// interceptor chain. When disabled, proto validation annotations are ignored.
DisableProtoValidate bool `envconfig:"DISABLE_PROTO_VALIDATE" default:"false"`
// DisableDebugLogInterceptor disables the DebugLogInterceptor in the default
// interceptor chain. When disabled, proto debug fields and metadata headers
// will not trigger per-request debug logging.
DisableDebugLogInterceptor bool `envconfig:"DISABLE_DEBUG_LOG_INTERCEPTOR" default:"false"`
// DebugLogHeaderName is the gRPC metadata / HTTP header name that triggers
// per-request debug logging. The header value should be a valid log level
// (e.g., "debug"). Default: "x-debug-log-level".
DebugLogHeaderName string `envconfig:"DEBUG_LOG_HEADER_NAME" default:"x-debug-log-level"`
// DisableVTProtobuf disables the use of the vtprotobuf marshaller and unmarshaller for GRPC
// https://github.com/planetscale/vtprotobuf
DisableVTProtobuf bool `envconfig:"DISABLE_VT_PROTOBUF" default:"false"`
Expand Down Expand Up @@ -210,6 +224,31 @@ func (c Config) Validate() []string {
if c.GRPCServerDefaultTimeoutInSeconds < 0 {
warnings = append(warnings, "GRPCServerDefaultTimeoutInSeconds is negative; use 0 to disable the timeout interceptor")
}
if c.GRPCTLSCertFile != "" && c.GRPCTLSKeyFile != "" {
if _, err := os.Stat(c.GRPCTLSCertFile); err != nil {
warnings = append(warnings, "GRPCTLSCertFile not found: "+c.GRPCTLSCertFile)
}
if _, err := os.Stat(c.GRPCTLSKeyFile); err != nil {
warnings = append(warnings, "GRPCTLSKeyFile not found: "+c.GRPCTLSKeyFile)
Comment thread
ankurs marked this conversation as resolved.
Outdated
}
}
if c.OTLPEndpoint != "" {
if _, _, err := net.SplitHostPort(c.OTLPEndpoint); err != nil {
warnings = append(warnings, "OTLPEndpoint should be in host:port format")
}
}
if c.LogLevel != "" {
switch strings.ToLower(c.LogLevel) {
case "error", "warn", "warning", "info", "debug":
// valid
default:
warnings = append(warnings, "LogLevel is not a recognized level: "+c.LogLevel)
}
Comment thread
ankurs marked this conversation as resolved.
}
if c.GRPCServerDefaultTimeoutInSeconds > 0 && c.ShutdownDurationInSeconds > 0 &&
c.GRPCServerDefaultTimeoutInSeconds > c.ShutdownDurationInSeconds {
warnings = append(warnings, "GRPCServerDefaultTimeoutInSeconds exceeds ShutdownDurationInSeconds; in-flight RPCs may be killed before timeout")
}

return warnings
}
98 changes: 98 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,101 @@ func TestValidateShutdownTiming(t *testing.T) {
t.Error("healthcheck duration >= shutdown duration should produce a warning")
}
}

func TestValidateTLSFileNotFound(t *testing.T) {
c := Config{
GRPCPort: 9090,
HTTPPort: 9091,
GRPCTLSCertFile: "/nonexistent/cert.pem",
GRPCTLSKeyFile: "/nonexistent/key.pem",
}
warnings := c.Validate()
foundCert := false
foundKey := false
for _, w := range warnings {
if strings.Contains(w, "GRPCTLSCertFile not found") {
foundCert = true
}
if strings.Contains(w, "GRPCTLSKeyFile not found") {
foundKey = true
}
}
if !foundCert || !foundKey {
t.Errorf("non-existent TLS files should produce warnings, got: %v", warnings)
}
}

func TestValidateOTLPEndpointFormat(t *testing.T) {
// Invalid endpoint
c := Config{
GRPCPort: 9090,
HTTPPort: 9091,
OTLPEndpoint: "not-a-host-port",
}
warnings := c.Validate()
found := false
for _, w := range warnings {
if strings.Contains(w, "host:port") {
found = true
}
}
if !found {
t.Error("invalid OTLP endpoint should produce a warning")
}

// Valid endpoint should not warn
c.OTLPEndpoint = "localhost:4317"
warnings = c.Validate()
for _, w := range warnings {
if strings.Contains(w, "host:port") {
t.Errorf("valid OTLP endpoint should not produce a warning, got: %s", w)
}
}
}

func TestValidateLogLevel(t *testing.T) {
// Invalid level
c := Config{
GRPCPort: 9090,
HTTPPort: 9091,
LogLevel: "trace",
}
warnings := c.Validate()
found := false
for _, w := range warnings {
if strings.Contains(w, "not a recognized level") {
found = true
}
}
if !found {
t.Error("invalid log level should produce a warning")
}

// Valid level should not warn
c.LogLevel = "debug"
warnings = c.Validate()
for _, w := range warnings {
if strings.Contains(w, "not a recognized level") {
t.Errorf("valid log level should not produce a warning, got: %s", w)
}
}
}

func TestValidateTimeoutExceedsShutdown(t *testing.T) {
c := Config{
GRPCPort: 9090,
HTTPPort: 9091,
GRPCServerDefaultTimeoutInSeconds: 120,
ShutdownDurationInSeconds: 15,
}
warnings := c.Validate()
found := false
for _, w := range warnings {
if strings.Contains(w, "exceeds ShutdownDurationInSeconds") {
found = true
}
}
if !found {
t.Error("timeout exceeding shutdown duration should produce a warning")
}
}
35 changes: 24 additions & 11 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@
if c.config.DisableProtoValidate {
interceptors.SetDisableProtoValidate(true)
}
if c.config.DisableDebugLogInterceptor {
interceptors.SetDisableDebugLogInterceptor(true)

Check failure on line 146 in core.go

View workflow job for this annotation

GitHub Actions / test

undefined: interceptors.SetDisableDebugLogInterceptor

Check failure on line 146 in core.go

View workflow job for this annotation

GitHub Actions / build

undefined: interceptors.SetDisableDebugLogInterceptor

Check failure on line 146 in core.go

View workflow job for this annotation

GitHub Actions / lint

undefined: interceptors.SetDisableDebugLogInterceptor
}
if c.config.DebugLogHeaderName != "" {
interceptors.SetDebugLogHeaderName(c.config.DebugLogHeaderName)

Check failure on line 149 in core.go

View workflow job for this annotation

GitHub Actions / test

undefined: interceptors.SetDebugLogHeaderName

Check failure on line 149 in core.go

View workflow job for this annotation

GitHub Actions / build

undefined: interceptors.SetDebugLogHeaderName

Check failure on line 149 in core.go

View workflow job for this annotation

GitHub Actions / lint

undefined: interceptors.SetDebugLogHeaderName (typecheck)
}
if c.config.EnablePrometheusGRPCHistogram {
if len(c.config.PrometheusGRPCHistogramBuckets) > 0 {
interceptors.SetServerMetricsOptions(
Expand Down Expand Up @@ -390,19 +396,26 @@
}
}

// getCustomHeaderMatcher returns a matcher that matches the given header and prefix
func getCustomHeaderMatcher(prefixes []string, header string) func(string) (string, bool) {
header = strings.ToLower(header)
// getCustomHeaderMatcher returns a matcher that matches the given exact headers and prefixes.
// Exact-match headers (e.g., trace header, debug log header) are forwarded from HTTP to gRPC metadata.
func getCustomHeaderMatcher(prefixes []string, headers ...string) func(string) (string, bool) {
lowerHeaders := make([]string, 0, len(headers))
for _, h := range headers {
if h != "" {
Comment thread
ankurs marked this conversation as resolved.
lowerHeaders = append(lowerHeaders, strings.ToLower(h))
}
}
return func(key string) (string, bool) {
key = strings.ToLower(key)

if key == header {
return key, true
} else if len(prefixes) > 0 {
for _, prefix := range prefixes {
if len(prefix) > 0 && strings.HasPrefix(key, strings.ToLower(prefix)) {
return key, true
}
for _, h := range lowerHeaders {
if key == h {
return key, true
}
}
for _, prefix := range prefixes {
if len(prefix) > 0 && strings.HasPrefix(key, strings.ToLower(prefix)) {
return key, true
}
}

Expand All @@ -425,7 +438,7 @@

muxOpts := []runtime.ServeMuxOption{
runtime.WithIncomingHeaderMatcher(
getCustomHeaderMatcher(allowedHttpHeaderPrefixes, c.config.TraceHeaderName),
getCustomHeaderMatcher(allowedHttpHeaderPrefixes, c.config.TraceHeaderName, c.config.DebugLogHeaderName),
Comment thread
ankurs marked this conversation as resolved.
),
runtime.WithMarshalerOption("application/proto", pMar),
runtime.WithMarshalerOption("application/protobuf", pMar),
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/goleak v1.3.0
golang.org/x/sync v0.20.0
Comment thread
ankurs marked this conversation as resolved.
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
Expand Down
27 changes: 27 additions & 0 deletions goleak_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package core

import (
"testing"

"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// gRPC callback serializers are started by resolver and balancer
// components during dial/server setup. They are cleaned up when the
// connection/server is closed, but some tests don't fully tear down.
goleak.IgnoreTopFunction("google.golang.org/grpc/internal/grpcsync.(*CallbackSerializer).run"),
// OTEL batch span processor runs a background queue for exporting
// spans. Started by TracerProvider in tests that configure OTLP.
goleak.IgnoreTopFunction("go.opentelemetry.io/otel/sdk/trace.(*batchSpanProcessor).processQueue"),
// sentry-go starts batch processor and HTTP transport worker goroutines
// when sentry.Init() is called in tests that set SentryDSN.
goleak.IgnoreTopFunction("github.com/getsentry/sentry-go.(*batchProcessor[...]).run"),
goleak.IgnoreTopFunction("github.com/getsentry/sentry-go.(*HTTPTransport).worker"),
// rollbar-go creates a global async client at package init time
// (rollbar.go:39: std = NewAsync(...)), starting a background goroutine
// unconditionally when the package is imported. Cannot be avoided.
goleak.IgnoreTopFunction("github.com/rollbar/rollbar-go.NewAsyncTransport.func1"),
)
}
Loading