httpapp

package
v0.0.0-...-f1ec9a5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

httpapp

httpapp assembles normal Adiom HTTP services with framework defaults.

Use httpapp.Service for internal Connect services. Use httpapp.App directly when a service needs a custom mix of Connect services and regular HTTP routes.

Service

runtime, err := httpapp.Init(
	context.Background(),
	httpapp.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
)
if err != nil {
	return err
}
defer runtime.Shutdown(context.Background())

return runtime.NewService(
	httpapp.WithServices(
		httpapp.ConnectHandler(
			appv1connect.AppServiceName,
			appv1connect.NewAppServiceHandler,
			api,
			httpapp.WithInterceptors(authInterceptor),
		),
	),
	httpapp.WithServiceRoutes(
		httpapp.Handle("/admin/", http.StripPrefix("/admin/", spa.DirHandler(webDir))),
	),
	httpapp.WithServiceReadinessChecks(
		httpapp.ReadinessCheck(db.Ping),
	),
	httpapp.WithReflection(),
).Run(runtime.Context())

Routes

Regular HTTP routes are explicit handlers:

httpapp.Handle("/oauth/callback", callbackHandler)

Wiring

Initialize the framework before constructing outbound clients. That lets client interceptors capture the real telemetry providers instead of the default noop providers.

runtime, err := httpapp.Init(context.Background())
if err != nil {
	return err
}
defer runtime.Shutdown(context.Background())

clientOpts, err := runtime.ConnectClientOptions()
if err != nil {
	return err
}
upstream := appv1connect.NewAppServiceClient(
	runtime.ConnectHTTPClient(),
	"http://upstream-service",
	clientOpts...,
)

api := NewAPI(upstream)

return runtime.NewService(
	httpapp.WithServices(
		httpapp.ConnectHandler(
			appv1connect.AppServiceName,
			appv1connect.NewAppServiceHandler,
			api,
		),
	),
	httpapp.WithServiceRoutes(
		httpapp.Handle("/admin/", http.StripPrefix("/admin/", spa.DirHandler(webDir))),
	),
).Run(runtime.Context())

Use runtime.Context() for server lifetime, runtime.ConnectClientOptions() with runtime.ConnectHTTPClient() for generated Connect clients, and runtime.HTTPClient() for raw outbound HTTP. Configure shared outbound HTTP settings once with httpapp.WithHTTPClient(...). Use runtime.NewService so the service shares the runtime telemetry configuration.

Defaults

Service and App automatically:

  • enable HTTP/1 and unencrypted HTTP/2 by default
  • install standard gRPC health
  • register liveness and readiness health labels for Kubernetes probes
  • recover panics and log requests
  • install OpenTelemetry HTTP server instrumentation
  • gracefully shut down when the context is canceled

Reflection is explicit and disabled by default.

If Addr is empty, the app listens on :$PORT when PORT is set, otherwise :8080.

TLS is opt-in. In the usual Kubernetes path, terminate TLS at the gateway or service mesh and keep service pods cleartext. If a service should serve TLS directly, configure certificate files explicitly:

httpapp.NewService(
	httpapp.WithServiceTLSFiles("/var/run/tls/tls.crt", "/var/run/tls/tls.key"),
)

or mount cert/key files and set:

TLS_CERT_FILE=/var/run/tls/tls.crt
TLS_KEY_FILE=/var/run/tls/tls.key

Both files must be configured together. Advanced services can pass a custom *tls.Config with httpapp.WithServiceTLSConfig(...).

Telemetry

httpapp.Init sets up signal handling, OpenTelemetry traces, metrics, and the default logger. Service.Run and App.Run also set up telemetry when Init was not called, so simple services still get telemetry automatically. The default OTLP/HTTP collector endpoint is http://otel-collector:4318, which matches the tenant namespace collector service. Exporters send traces to /v1/traces and metrics to /v1/metrics.

Connect services get connectrpc.com/otelconnect instrumentation automatically, including RPC spans, trace context propagation, and RPC metrics such as rpc.server.duration, request size, response size, and messages per RPC. Regular HTTP routes get otelhttp instrumentation. Connect routes are filtered out of the generic HTTP middleware so they do not emit duplicate HTTP and RPC server spans.

The framework propagator is W3C Trace Context plus Baggage. Incoming Connect trace contexts are trusted so gateway and service spans stay in one trace. For outbound generated Connect clients, pass telemetry.ConnectClientOptions into the client constructor, or use runtime.ConnectClientOptions() after httpapp.Init. Pass runtime.ConnectHTTPClient() to generated Connect clients; the Connect interceptor emits RPC client telemetry and propagates trace context. For raw outbound HTTP, use runtime.HTTPClient() or telemetry.HTTPClient.

Root trace sampling is parent-based and defaults to sampling all new root traces. Downstream services honor the upstream sampled bit. Configure root sampling with SampleRatioValue, including explicit 0% sampling:

runtime, err := httpapp.Init(
	context.Background(),
	httpapp.WithTelemetry(telemetry.Config{
		ServiceName: "platform-manager",
		SampleRatio: telemetry.SampleRatioValue(0.10),
	}),
)

Use telemetry.SampleRatioValue(0) for 0% root sampling while still honoring sampled upstream parents.

Add custom spans around application operations with a stable span name:

ctx, span := telemetry.StartSpan(ctx, "CreateNamespace")
defer span.End()

The framework derives the instrumentation scope from the caller package. Span names should be bounded operation names, not user input, IDs, or raw paths.

httpapp.Init installs a structured default logger wrapped with telemetry.TraceLogHandler, so package-level slog.InfoContext(ctx, ...) includes framework log fields and trace correlation. To customize the logger, pass one to Init:

logger := telemetry.NewLogger(slog.NewJSONHandler(os.Stdout, nil))

runtime, err := httpapp.Init(context.Background(), httpapp.WithLogger(logger))
if err != nil {
	return err
}
defer runtime.Shutdown(context.Background())

return runtime.NewService(
	// ...
).Run(runtime.Context())

To suppress low-severity application logs when the current trace is not sampled, wrap the output handler with telemetry.SampledLogHandler:

logger := telemetry.NewLogger(
	telemetry.SampledLogHandler(slog.NewJSONHandler(os.Stdout, nil)),
)

By default, unsampled traces suppress logs below Warn; warnings, errors, sampled traces, and logs without trace context still emit.

The default logger installed by httpapp.Init also honors:

LOG_LEVEL=debug|info|warn|error
LOG_UNSAMPLED_MIN_LEVEL=debug|info|warn|error|off

LOG_LEVEL is the global minimum log level. LOG_UNSAMPLED_MIN_LEVEL adds a separate minimum for records whose context contains a valid unsampled trace; set it to off to suppress all such records.

Request-specific log fields live in ctx. Use ContextWithLogAttrs as middleware and handlers learn more scope, then log normally with any wrapped logger:

ctx = telemetry.ContextWithLogAttrs(ctx, "tenant_id", tenantID)

logger.InfoContext(ctx, "loaded workspace", "workspace_id", workspaceID)

If the framework logger is the default logger, package-level slog calls work too:

slog.InfoContext(ctx, "loaded workspace", "workspace_id", workspaceID)

Components with their own logger should use a logger built from telemetry.TraceLogHandler or derived from the service logger with logger.With(...), then call InfoContext, ErrorContext, and the other context-aware slog methods. Static component fields can live on the logger; request fields and trace/span IDs come from ctx.

For APIs that accept a logger but do not pass ctx to each log call, snapshot the current context into a child logger:

scopedLogger := telemetry.LoggerWithContext(ctx, logger)
component := NewComponent(scopedLogger)

Use that form only at boundaries with context-less logging APIs. In normal request code, prefer InfoContext(ctx, ...) so child spans and added context fields are read at the moment of the log call.

When ctx contains an active span, the logger adds trace_id, span_id, and trace_sampled fields at log-call time. The log collector should parse JSON logs and preserve those fields so the log backend can link or query logs by trace ID.

Framework telemetry keeps built-in metric dimensions low-cardinality. HTTP server metrics keep method, status, and route pattern. HTTP client metrics keep method and status. RPC metrics keep system, service, method, and status/error code. Connect trace events and peer address attributes are omitted by default.

The service name defaults to OTEL_SERVICE_NAME, then the executable name. Set it explicitly when constructing services:

httpapp.NewService(
	httpapp.WithServiceTelemetry(telemetry.DefaultConfig("platform-manager")),
)

Useful environment overrides:

  • OTEL_SDK_DISABLED=true disables framework telemetry.
  • OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 overrides the base endpoint.
  • OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_METRICS_ENDPOINT override individual signal endpoints.
  • OTEL_TRACES_EXPORTER=none or OTEL_METRICS_EXPORTER=none disables one signal.

For local tests or tools that should stay quiet:

httpapp.NewService(
	httpapp.WithServiceTelemetry(telemetry.DisabledConfig()),
)

Kubernetes Health

Use Kubernetes gRPC probes against the same service port:

livenessProbe:
  grpc:
    port: 8080
    service: liveness
readinessProbe:
  grpc:
    port: 8080
    service: readiness

ReadinessChecks back the readiness health label. Kubernetes does not need to know the individual dependency names.

LivenessChecks are also available, but leave them empty unless the service has a concrete stuck-process condition that should restart the pod. With no checks, both liveness and readiness return serving.

Customization

Common Connect interceptors apply to every Connect service:

httpapp.NewService(
	httpapp.WithServiceInterceptors(traceInterceptor),
)

Service-specific interceptors apply after common interceptors:

httpapp.ConnectHandler(
	userv1connect.UserServiceName,
	userv1connect.NewUserServiceHandler,
	userSvc,
	httpapp.WithInterceptors(authInterceptor),
)

Use HTTP middleware for regular HTTP concerns:

stack := append(middleware.Default(logger), cors, addRequestID)

httpapp.NewService(
	httpapp.WithServiceMiddleware(stack...),
)

Tune server timeouts when a service needs to override defaults:

httpapp.NewService(
	httpapp.WithServiceIdleTimeout(75 * time.Second),
)

Per-route and per-service middleware wraps only that route or service. Standard gRPC health bypasses HTTP middleware so Kubernetes probes do not need application credentials.

Documentation

Index

Constants

View Source
const (
	// DefaultAddr is the default listen address for HTTP apps.
	DefaultAddr = httpserver.DefaultAddr

	// LivenessService is the Kubernetes gRPC health service label for liveness.
	LivenessService = httpserver.LivenessService
	// ReadinessService is the Kubernetes gRPC health service label for readiness.
	ReadinessService = httpserver.ReadinessService
)

Variables

This section is empty.

Functions

func SignalContext

func SignalContext(parent context.Context) (context.Context, context.CancelFunc)

SignalContext returns a context canceled on SIGINT or SIGTERM.

Types

type App

type App struct {
	Addr              string
	Routes            []Route
	Connect           []ConnectService
	Reflection        bool
	LivenessChecks    []Check
	ReadinessChecks   []Check
	Interceptors      []connect.Interceptor
	ConnectOptions    []connect.HandlerOption
	Middleware        []Middleware
	Logger            *slog.Logger
	Telemetry         telemetry.Config
	ReadHeaderTimeout time.Duration
	IdleTimeout       time.Duration
	ShutdownTimeout   time.Duration
	TLSConfig         *tls.Config
	TLSCertFile       string
	TLSKeyFile        string
}

App is the shared assembly base used by Service. Prefer NewService for normal internal services.

func (App) Handler

func (a App) Handler() http.Handler

Handler assembles the application handler.

func (App) Run

func (a App) Run(ctx context.Context) error

Run assembles and runs the app server.

type Check

type Check = httpserver.Check

Check is a health dependency check.

func LivenessCheck

func LivenessCheck(fn func(context.Context) error) Check

LivenessCheck adapts fn into a liveness check.

func ReadinessCheck

func ReadinessCheck(fn func(context.Context) error) Check

ReadinessCheck adapts fn into a readiness check.

type ConnectOption

type ConnectOption func(*ConnectService)

ConnectOption customizes a Connect service.

func WithConnectMiddleware

func WithConnectMiddleware(middleware ...Middleware) ConnectOption

WithConnectMiddleware adds HTTP middleware to a Connect service.

func WithConnectOptions

func WithConnectOptions(options ...connect.HandlerOption) ConnectOption

WithConnectOptions adds Connect handler options to a Connect service.

func WithInterceptors

func WithInterceptors(interceptors ...connect.Interceptor) ConnectOption

WithInterceptors adds Connect interceptors to a Connect service.

type ConnectService

type ConnectService struct {
	Name           string
	NewHandler     func(...connect.HandlerOption) (string, http.Handler)
	Interceptors   []connect.Interceptor
	ConnectOptions []connect.HandlerOption
	Middleware     []Middleware
}

ConnectService is a generated Connect service handler factory plus service specific options.

func Connect

func Connect(name string, newHandler func(...connect.HandlerOption) (string, http.Handler), opts ...ConnectOption) ConnectService

Connect builds a Connect service registration.

func ConnectHandler

func ConnectHandler[T any](
	name string,
	newHandler func(T, ...connect.HandlerOption) (string, http.Handler),
	implementation T,
	opts ...ConnectOption,
) ConnectService

ConnectHandler builds a Connect service registration from a generated Connect handler constructor and service implementation.

type InitOption

type InitOption func(*initConfig)

InitOption customizes process-level framework initialization.

func WithHTTPClient

func WithHTTPClient(client *http.Client) InitOption

WithHTTPClient sets the runtime's base outbound HTTP client.

func WithLogger

func WithLogger(logger *slog.Logger) InitOption

WithLogger overrides the process default logger installed by Init.

func WithTelemetry

func WithTelemetry(cfg telemetry.Config) InitOption

WithTelemetry overrides the default telemetry configuration for Init.

type Middleware

type Middleware = httpmiddleware.Middleware

Middleware wraps an HTTP handler. The first middleware in a list is outermost.

type Route

type Route struct {
	Pattern    string
	Handler    http.Handler
	Middleware []Middleware
}

Route is a regular HTTP route.

func Handle

func Handle(pattern string, handler http.Handler, opts ...RouteOption) Route

Handle builds a regular HTTP route.

type RouteOption

type RouteOption func(*Route)

RouteOption customizes a route.

func WithMiddleware

func WithMiddleware(middleware ...Middleware) RouteOption

WithMiddleware adds HTTP middleware to a route.

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime holds process-level framework initialization.

func Init

func Init(ctx context.Context, opts ...InitOption) (Runtime, error)

Init performs process-level framework initialization.

func (Runtime) ConnectClientOptions

func (r Runtime) ConnectClientOptions() ([]connect.ClientOption, error)

ConnectClientOptions returns Connect client options for RPC telemetry.

func (Runtime) ConnectHTTPClient

func (r Runtime) ConnectHTTPClient(client ...*http.Client) *http.Client

ConnectHTTPClient returns a runtime outbound client for generated Connect clients. Passing a client overrides the runtime base client for this call. Connect telemetry comes from ConnectClientOptions, so this does not wrap the transport with HTTP telemetry.

func (Runtime) Context

func (r Runtime) Context() context.Context

Context returns the runtime context canceled on SIGINT, SIGTERM, parent cancellation, or Shutdown.

func (Runtime) HTTPClient

func (r Runtime) HTTPClient(client ...*http.Client) *http.Client

HTTPClient returns a runtime outbound client whose transport emits HTTP telemetry. Passing a client overrides the runtime base client for this call.

func (Runtime) Logger

func (r Runtime) Logger() *slog.Logger

Logger returns the runtime logger.

func (Runtime) NewService

func (r Runtime) NewService(opts ...ServiceOption) Service

NewService builds a Service using the runtime telemetry configuration.

func (Runtime) Shutdown

func (r Runtime) Shutdown(ctx context.Context) error

Shutdown flushes and stops initialized framework resources.

func (Runtime) TelemetryConfig

func (r Runtime) TelemetryConfig() telemetry.Config

TelemetryConfig returns the runtime telemetry configuration.

type Service

type Service struct {
	Addr              string
	Connect           []ConnectService
	Routes            []Route
	Reflection        bool
	LivenessChecks    []Check
	ReadinessChecks   []Check
	Interceptors      []connect.Interceptor
	ConnectOptions    []connect.HandlerOption
	Middleware        []Middleware
	Logger            *slog.Logger
	Telemetry         telemetry.Config
	ReadHeaderTimeout time.Duration
	IdleTimeout       time.Duration
	ShutdownTimeout   time.Duration
	TLSConfig         *tls.Config
	TLSCertFile       string
	TLSKeyFile        string
}

Service is the normal shape for an internal Connect service.

func NewService

func NewService(opts ...ServiceOption) Service

NewService builds an internal Connect service with framework defaults.

func (Service) Handler

func (s Service) Handler() http.Handler

Handler assembles the service handler.

func (Service) Run

func (s Service) Run(ctx context.Context) error

Run assembles and runs the service.

type ServiceOption

type ServiceOption func(*Service)

ServiceOption customizes a Service.

func WithReflection

func WithReflection() ServiceOption

WithReflection enables explicit Connect reflection.

func WithServiceAddr

func WithServiceAddr(addr string) ServiceOption

WithServiceAddr overrides the default listen address.

func WithServiceConnectOptions

func WithServiceConnectOptions(options ...connect.HandlerOption) ServiceOption

WithServiceConnectOptions adds common Connect handler options.

func WithServiceIdleTimeout

func WithServiceIdleTimeout(timeout time.Duration) ServiceOption

WithServiceIdleTimeout overrides the server idle timeout.

func WithServiceInterceptors

func WithServiceInterceptors(interceptors ...connect.Interceptor) ServiceOption

WithServiceInterceptors adds common Connect interceptors.

func WithServiceLivenessChecks

func WithServiceLivenessChecks(checks ...Check) ServiceOption

WithServiceLivenessChecks adds liveness checks.

func WithServiceLogger

func WithServiceLogger(logger *slog.Logger) ServiceOption

WithServiceLogger sets the service logger.

func WithServiceMiddleware

func WithServiceMiddleware(middleware ...Middleware) ServiceOption

WithServiceMiddleware replaces the service HTTP middleware stack.

func WithServiceReadinessChecks

func WithServiceReadinessChecks(checks ...Check) ServiceOption

WithServiceReadinessChecks adds readiness checks.

func WithServiceRoutes

func WithServiceRoutes(routes ...Route) ServiceOption

WithServiceRoutes adds regular HTTP routes.

func WithServiceTLSConfig

func WithServiceTLSConfig(cfg *tls.Config) ServiceOption

WithServiceTLSConfig enables TLS with an application-provided TLS config.

func WithServiceTLSFiles

func WithServiceTLSFiles(certFile, keyFile string) ServiceOption

WithServiceTLSFiles enables TLS with certificate and private key files.

func WithServiceTelemetry

func WithServiceTelemetry(cfg telemetry.Config) ServiceOption

WithServiceTelemetry overrides the service OpenTelemetry configuration.

func WithServiceTimeouts

func WithServiceTimeouts(readHeaderTimeout, shutdownTimeout time.Duration) ServiceOption

WithServiceTimeouts overrides server timeouts.

func WithServices

func WithServices(services ...ConnectService) ServiceOption

WithServices adds Connect services.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL