diff --git a/README.md b/README.md index af289db..688a877 100755 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ For full documentation, visit https://docs.coldbrew.cloud ## Index - [Constants](<#constants>) -- [func ConfigureInterceptors\(DoNotLogGRPCReflection bool, traceHeaderName string, responseTimeLogLevel string, responseTimeLogErrorOnly bool\)](<#ConfigureInterceptors>) - [func InitializeVTProto\(\)](<#InitializeVTProto>) - [func OTELMeterProvider\(\) otelmetric.MeterProvider](<#OTELMeterProvider>) - [func SetOTELGRPCClientOptions\(opts ...otelgrpc.Option\)](<#SetOTELGRPCClientOptions>) @@ -117,17 +116,8 @@ For full documentation, visit https://docs.coldbrew.cloud const SupportPackageIsVersion1 = true ``` - -## func [ConfigureInterceptors]() - -```go -func ConfigureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string, responseTimeLogLevel string, responseTimeLogErrorOnly bool) -``` - -ConfigureInterceptors configures the interceptors package with the provided settings. - -## func [InitializeVTProto]() +## func [InitializeVTProto]() ```go func InitializeVTProto() @@ -138,7 +128,7 @@ InitializeVTProto initializes the vtproto package for use with the service https://github.com/planetscale/vtprotobuf?tab=readme-ov-file#mixing-protobuf-implementations-with-grpc -## func [OTELMeterProvider]() +## func [OTELMeterProvider]() ```go func OTELMeterProvider() otelmetric.MeterProvider @@ -147,7 +137,7 @@ func OTELMeterProvider() otelmetric.MeterProvider OTELMeterProvider returns the global OTel MeterProvider. This is a convenience accessor for code that needs the interface type. -## func [SetOTELGRPCClientOptions]() +## func [SetOTELGRPCClientOptions]() ```go func SetOTELGRPCClientOptions(opts ...otelgrpc.Option) @@ -156,7 +146,7 @@ func SetOTELGRPCClientOptions(opts ...otelgrpc.Option) Deprecated: Use SetOTELOptions instead. Only applies when OTEL\_USE\_LEGACY\_INSTRUMENTATION=true. -## func [SetOTELGRPCServerOptions]() +## func [SetOTELGRPCServerOptions]() ```go func SetOTELGRPCServerOptions(opts ...otelgrpc.Option) @@ -165,7 +155,7 @@ func SetOTELGRPCServerOptions(opts ...otelgrpc.Option) Deprecated: Use SetOTELOptions instead. Only applies when OTEL\_USE\_LEGACY\_INSTRUMENTATION=true. -## func [SetOTELOptions]() +## func [SetOTELOptions]() ```go func SetOTELOptions(opts grpcotel.Options) @@ -174,7 +164,7 @@ func SetOTELOptions(opts grpcotel.Options) SetOTELOptions configures the native gRPC stats/opentelemetry integration. Must be called during init, before the gRPC server starts. When set, processConfig\(\) will NOT overwrite these with auto\-built options. -## func [SetupAutoMaxProcs]() +## func [SetupAutoMaxProcs]() ```go func SetupAutoMaxProcs() @@ -192,7 +182,7 @@ func SetupEnvironment(env string) SetupEnvironment sets the environment This is used to identify the environment in Sentry and New Relic env is the environment to set for the service \(e.g. prod, staging, dev\) -## func [SetupHystrixPrometheus]() +## func [SetupHystrixPrometheus]() ```go func SetupHystrixPrometheus() @@ -210,7 +200,7 @@ func SetupLogger(logLevel string, jsonlogs bool) error SetupLogger sets up the logger It uses the coldbrew logger to log messages to stdout logLevel is the log level to set for the logger jsonlogs is a boolean to enable or disable json logs -## func [SetupNROpenTelemetry]() +## func [SetupNROpenTelemetry]() ```go func SetupNROpenTelemetry(serviceName, license, version string, ratio float64) error @@ -237,7 +227,7 @@ func SetupNewRelic(serviceName, apiKey string, tracing bool) error SetupNewRelic sets up the New Relic tracing and monitoring agent for the service It uses the New Relic Go Agent to send traces to New Relic One APM and Insights serviceName is the name of the service apiKey is the New Relic license key tracing is a boolean to enable or disable tracing -## func [SetupOTELMetrics]() +## func [SetupOTELMetrics]() ```go func SetupOTELMetrics(config OTLPConfig, interval time.Duration) (*sdkmetric.MeterProvider, error) @@ -248,7 +238,7 @@ SetupOTELMetrics creates a MeterProvider with an OTLP gRPC exporter that reuses Call this after SetupOpenTelemetry so the shared resource is available. -## func [SetupOpenTelemetry]() +## func [SetupOpenTelemetry]() ```go func SetupOpenTelemetry(config OTLPConfig) error @@ -324,7 +314,7 @@ type CB interface { ``` -### func [New]() +### func [New]() ```go func New(c config.Config) CB diff --git a/config/README.md b/config/README.md index 7f87c78..3322bb7 100755 --- a/config/README.md +++ b/config/README.md @@ -66,7 +66,7 @@ import "github.com/go-coldbrew/core/config" -## type [Config]() +## type [Config]() 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 @@ -144,19 +144,19 @@ type Config struct { // MaxConnectionIdle is a duration for the amount of time after which an // idle connection would be closed by sending a GoAway. Idleness duration is // defined since the most recent time the number of outstanding RPCs became - // zero or the connection establishment. + // zero or the connection establishment. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionIdleInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS"` + GRPCServerMaxConnectionIdleInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS" default:"300"` // MaxConnectionAge is a duration for the maximum amount of time a // connection may exist before it will be closed by sending a GoAway. A // random jitter of +/-10% will be added to MaxConnectionAge to spread out - // connection storms. + // connection storms. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionAgeInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS"` + GRPCServerMaxConnectionAgeInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS" default:"1800"` // MaxConnectionAgeGrace is an additive period after MaxConnectionAge after - // which the connection will be forcibly closed. + // which the connection will be forcibly closed. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionAgeGraceInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS"` + GRPCServerMaxConnectionAgeGraceInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS" default:"30"` // DisableAutoMaxProcs disables the automatic setting of GOMAXPROCS // This is useful when running in a container where the container runtime sets GOMAXPROCS for you already @@ -181,6 +181,10 @@ type Config struct { // sizes for sending and receiving messages over GRPC GRPCMaxSendMsgSize int `envconfig:"GRPC_MAX_SEND_MSG_SIZE" default:"2147483647"` // Unlimited GRPCMaxRecvMsgSize int `envconfig:"GRPC_MAX_RECV_MSG_SIZE" default:"4194304"` // 4MB + // GRPCServerDefaultTimeoutInSeconds is the default timeout (in seconds) for + // incoming unary gRPC requests that arrive without a deadline. Set to 0 to + // disable. Does not apply to stream RPCs. + GRPCServerDefaultTimeoutInSeconds int `envconfig:"GRPC_SERVER_DEFAULT_TIMEOUT_IN_SECONDS" default:"60"` // OTLPEndpoint is the OTLP gRPC endpoint to send traces to // Examples: "localhost:4317", "api.honeycomb.io:443", "otel-collector:4317" @@ -234,7 +238,7 @@ type Config struct { ``` -### func \(Config\) [Validate]() +### func \(Config\) [Validate]() ```go func (c Config) Validate() []string diff --git a/config/config.go b/config/config.go index b54d4c2..1e4b39e 100644 --- a/config/config.go +++ b/config/config.go @@ -76,19 +76,19 @@ type Config struct { // MaxConnectionIdle is a duration for the amount of time after which an // idle connection would be closed by sending a GoAway. Idleness duration is // defined since the most recent time the number of outstanding RPCs became - // zero or the connection establishment. + // zero or the connection establishment. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionIdleInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS"` + GRPCServerMaxConnectionIdleInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_IDLE_IN_SECONDS" default:"300"` // MaxConnectionAge is a duration for the maximum amount of time a // connection may exist before it will be closed by sending a GoAway. A // random jitter of +/-10% will be added to MaxConnectionAge to spread out - // connection storms. + // connection storms. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionAgeInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS"` + GRPCServerMaxConnectionAgeInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_IN_SECONDS" default:"1800"` // MaxConnectionAgeGrace is an additive period after MaxConnectionAge after - // which the connection will be forcibly closed. + // which the connection will be forcibly closed. Set to -1 to disable (infinite). // https://github.com/grpc/grpc-go/blob/v1.48.0/keepalive/keepalive.go#L50 - GRPCServerMaxConnectionAgeGraceInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS"` + GRPCServerMaxConnectionAgeGraceInSeconds int `envconfig:"GRPC_SERVER_MAX_CONNECTION_AGE_GRACE_IN_SECONDS" default:"30"` // DisableAutoMaxProcs disables the automatic setting of GOMAXPROCS // This is useful when running in a container where the container runtime sets GOMAXPROCS for you already @@ -113,6 +113,10 @@ type Config struct { // sizes for sending and receiving messages over GRPC GRPCMaxSendMsgSize int `envconfig:"GRPC_MAX_SEND_MSG_SIZE" default:"2147483647"` // Unlimited GRPCMaxRecvMsgSize int `envconfig:"GRPC_MAX_RECV_MSG_SIZE" default:"4194304"` // 4MB + // GRPCServerDefaultTimeoutInSeconds is the default timeout (in seconds) for + // incoming unary gRPC requests that arrive without a deadline. Set to 0 to + // disable. Does not apply to stream RPCs. + GRPCServerDefaultTimeoutInSeconds int `envconfig:"GRPC_SERVER_DEFAULT_TIMEOUT_IN_SECONDS" default:"60"` // Custom OpenTelemetry OTLP Configuration // When OTLPEndpoint is set, it takes precedence over NewRelic OpenTelemetry configuration @@ -203,6 +207,9 @@ func (c Config) Validate() []string { if c.EnableOTELMetrics && c.OTELMetricsInterval <= 0 { warnings = append(warnings, "OTELMetricsInterval should be positive when ENABLE_OTEL_METRICS is true") } + if c.GRPCServerDefaultTimeoutInSeconds < 0 { + warnings = append(warnings, "GRPCServerDefaultTimeoutInSeconds is negative; use 0 to disable the timeout interceptor") + } return warnings } diff --git a/core.go b/core.go index 2694d3d..b5650b5 100644 --- a/core.go +++ b/core.go @@ -131,7 +131,7 @@ func (c *cb) processConfig() { SetupEnvironment(c.config.Environment) SetupReleaseName(c.config.ReleaseName) SetupHystrixPrometheus() - ConfigureInterceptors(c.config.DoNotLogGRPCReflection, c.config.TraceHeaderName, c.config.ResponseTimeLogLevel, c.config.ResponseTimeLogErrorOnly) + configureInterceptors(c.config.DoNotLogGRPCReflection, c.config.TraceHeaderName, c.config.ResponseTimeLogLevel, c.config.ResponseTimeLogErrorOnly, c.config.GRPCServerDefaultTimeoutInSeconds) if !c.config.DisableSignalHandler { dur := time.Second * 10 if c.config.ShutdownDurationInSeconds > 0 { @@ -628,9 +628,9 @@ func (c *cb) getGRPCServerOptions() []grpc.ServerOption { so = append(so, grpc.MaxSendMsgSize(c.config.GRPCMaxSendMsgSize)) } - if c.config.GRPCServerMaxConnectionAgeGraceInSeconds > 0 || - c.config.GRPCServerMaxConnectionAgeInSeconds > 0 || - c.config.GRPCServerMaxConnectionIdleInSeconds > 0 { + if c.config.GRPCServerMaxConnectionAgeGraceInSeconds != 0 || + c.config.GRPCServerMaxConnectionAgeInSeconds != 0 || + c.config.GRPCServerMaxConnectionIdleInSeconds != 0 { option := keepalive.ServerParameters{} if c.config.GRPCServerMaxConnectionIdleInSeconds > 0 { option.MaxConnectionIdle = time.Duration( diff --git a/core_coverage_test.go b/core_coverage_test.go index 7176694..43ff707 100644 --- a/core_coverage_test.go +++ b/core_coverage_test.go @@ -449,12 +449,78 @@ func TestGetCustomHeaderMatcher_EmptyPrefix(t *testing.T) { // --- Group 2: gRPC Server Options --- -func TestGetGRPCServerOptions_Default(t *testing.T) { +func testKeepaliveBaseline() int { + base := &cb{config: config.Config{}} + return len(base.getGRPCServerOptions()) +} + +func TestGetGRPCServerOptions_WithEnvconfigDefaults(t *testing.T) { // removed t.Parallel() — core tests mutate package-level globals - c := &cb{config: config.Config{}} + // Validates behavior when config has the envconfig default values (300, 1800, 30). + // Keepalive option should be appended beyond the baseline. + baseline := testKeepaliveBaseline() + c := &cb{config: config.Config{ + GRPCServerMaxConnectionIdleInSeconds: 300, + GRPCServerMaxConnectionAgeInSeconds: 1800, + GRPCServerMaxConnectionAgeGraceInSeconds: 30, + }} opts := c.getGRPCServerOptions() - if len(opts) < 2 { - t.Fatalf("expected at least 2 server options, got %d", len(opts)) + if len(opts) <= baseline { + t.Fatalf("expected keepalive option beyond baseline %d, got %d", baseline, len(opts)) + } +} + +func TestGetGRPCServerOptions_KeepaliveDisabledWithNegativeOne(t *testing.T) { + // removed t.Parallel() — core tests mutate package-level globals + // Setting all values to -1 should still append keepalive params (outer != 0 check), + // but individual parameters are not set on ServerParameters (inner > 0 check), + // so gRPC uses infinity for each. + baseline := testKeepaliveBaseline() + c := &cb{config: config.Config{ + GRPCServerMaxConnectionIdleInSeconds: -1, + GRPCServerMaxConnectionAgeInSeconds: -1, + GRPCServerMaxConnectionAgeGraceInSeconds: -1, + }} + opts := c.getGRPCServerOptions() + if len(opts) <= baseline { + t.Fatalf("expected keepalive option beyond baseline %d, got %d", baseline, len(opts)) + } +} + +func TestGetGRPCServerOptions_KeepaliveMixed(t *testing.T) { + // removed t.Parallel() — core tests mutate package-level globals + // Mix of -1 (disabled) and positive (enabled) values should still append + // keepalive params relative to the all-zero baseline. + baseline := testKeepaliveBaseline() + c := &cb{config: config.Config{ + GRPCServerMaxConnectionIdleInSeconds: -1, + GRPCServerMaxConnectionAgeInSeconds: 1800, + GRPCServerMaxConnectionAgeGraceInSeconds: 30, + }} + opts := c.getGRPCServerOptions() + if len(opts) <= baseline { + t.Fatalf("expected keepalive option beyond baseline %d, got %d", baseline, len(opts)) + } +} + +func TestGetGRPCServerOptions_KeepaliveAllZero(t *testing.T) { + // removed t.Parallel() — core tests mutate package-level globals + // All zeros should NOT add keepalive params (outer != 0 check is false). + // Compare against a positive-value config to prove zero omits keepalive. + zeroConfig := &cb{config: config.Config{ + GRPCServerMaxConnectionIdleInSeconds: 0, + GRPCServerMaxConnectionAgeInSeconds: 0, + GRPCServerMaxConnectionAgeGraceInSeconds: 0, + }} + positiveConfig := &cb{config: config.Config{ + GRPCServerMaxConnectionIdleInSeconds: 300, + GRPCServerMaxConnectionAgeInSeconds: 1800, + GRPCServerMaxConnectionAgeGraceInSeconds: 30, + }} + zeroOpts := zeroConfig.getGRPCServerOptions() + positiveOpts := positiveConfig.getGRPCServerOptions() + if len(zeroOpts) >= len(positiveOpts) { + t.Fatalf("expected fewer options with all-zero keepalive than positive, got %d vs %d", len(zeroOpts), len(positiveOpts)) } } @@ -1302,8 +1368,21 @@ func TestSetupOpenTelemetry_MissingServiceName(t *testing.T) { } } -func TestConfigureInterceptors_BothBranches(t *testing.T) { - ConfigureInterceptors(true, "X-My-Trace", "info", false) +func Test_configureInterceptors_BothBranches(t *testing.T) { + tests := []struct { + name string + defaultTimeoutInSeconds int + }{ + {name: "timeout enabled", defaultTimeoutInSeconds: 60}, + {name: "timeout disabled", defaultTimeoutInSeconds: 0}, + {name: "timeout negative", defaultTimeoutInSeconds: -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configureInterceptors(true, "X-My-Trace", "info", false, tt.defaultTimeoutInSeconds) + }) + } } func TestConfig_Validate_HTTPCompressionMinSize(t *testing.T) { diff --git a/go.mod b/go.mod index ab5026d..0890f5e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/go-coldbrew/core -go 1.25.8 +go 1.25.9 require ( github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 github.com/go-coldbrew/errors v0.2.13 github.com/go-coldbrew/hystrixprometheus v0.1.2 - github.com/go-coldbrew/interceptors v0.1.20 + github.com/go-coldbrew/interceptors v0.1.22 github.com/go-coldbrew/log v0.3.1 github.com/go-coldbrew/options v0.3.0 github.com/go-coldbrew/tracing v0.2.0 diff --git a/go.sum b/go.sum index 687c98a..0e8a955 100644 --- a/go.sum +++ b/go.sum @@ -197,8 +197,8 @@ github.com/go-coldbrew/errors v0.2.13 h1:OUOEWLml6Mstt0Sskc94VVPhZ/rMfONeNOlHqyc github.com/go-coldbrew/errors v0.2.13/go.mod h1:eFLqeTPhgGyvsVVRXcdKGUxEnm+chrpddhL8lfogonk= github.com/go-coldbrew/hystrixprometheus v0.1.2 h1:WSt4FtYr8xNDKgdGWYpMfXGFIK7zdDSBwDSbpuPhBHI= github.com/go-coldbrew/hystrixprometheus v0.1.2/go.mod h1:OrNRHHxZagpmQXNp//oHKOemGSU0ScOqEcJgeKbJ+wg= -github.com/go-coldbrew/interceptors v0.1.20 h1:8AmKCcqkupJdZhG6tCaswDIwQBgX5MKWCig7EAXhSGo= -github.com/go-coldbrew/interceptors v0.1.20/go.mod h1:QmjjBvZMrIP0UvyYRbKNjcKnMUegsJ3k76CMu0poQXQ= +github.com/go-coldbrew/interceptors v0.1.22 h1:gglnX6iFl1unC4CoJRauwEmw+ldr5ZsLdxmqhE38Kk8= +github.com/go-coldbrew/interceptors v0.1.22/go.mod h1:qR1CzRxSemxlo+5UhSBYmNbfycRRIWwTO7zLgf3E6GE= github.com/go-coldbrew/log v0.3.1 h1:Cyx6KWBW3wZE8dSru6mIDFtUnJ1R2h6C44ZDo5bOqAo= github.com/go-coldbrew/log v0.3.1/go.mod h1:xxZGHBfni5eXc6Azg+g8UPTmqTJLAf9sX46gAT8o39Y= github.com/go-coldbrew/options v0.3.0 h1:JwyVntb9bzBeFdaHFK6yGVVz30G3aVlqJJ6uVyYQfCc= diff --git a/initializers.go b/initializers.go index a832ddc..0806621 100644 --- a/initializers.go +++ b/initializers.go @@ -385,8 +385,8 @@ func SetupHystrixPrometheus() { }) } -// ConfigureInterceptors configures the interceptors package with the provided settings. -func ConfigureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string, responseTimeLogLevel string, responseTimeLogErrorOnly bool) { +// configureInterceptors configures the interceptors package with the provided settings. +func configureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string, responseTimeLogLevel string, responseTimeLogErrorOnly bool, defaultTimeoutInSeconds int) { if DoNotLogGRPCReflection { methods := append(interceptors.FilterMethods, "grpc.reflection.v1alpha.ServerReflection") //nolint:staticcheck // FilterMethods read is fine, using SetFilterMethods to write interceptors.SetFilterMethods(context.Background(), methods) @@ -403,6 +403,11 @@ func ConfigureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string, interceptors.SetResponseTimeLogLevel(context.Background(), level) } interceptors.SetResponseTimeLogErrorOnly(responseTimeLogErrorOnly) + if defaultTimeoutInSeconds > 0 { + interceptors.SetDefaultTimeout(time.Second * time.Duration(defaultTimeoutInSeconds)) + } else { + interceptors.SetDefaultTimeout(0) + } } // SetupAutoMaxProcs sets up the GOMAXPROCS to match Linux container CPU quota