Sprout
Sprout is a module to build microservices in Go. It provides a way to set up
shared things such as configuration, logging, tracing, metrics and health
checks.
Features
- 💉 Dependency injection and lifecycle management via Fx
- 🛠️ Configuration via environment variables using env
- 📝 Logging via Zap and logr
- 🔍 Tracing and metrics via OpenTelemetry
- 🩺 Liveness and readiness checks via Health
- 📤 OTLP exporting of traces and metrics
Usage
Sprout provides a small wrapper around Fx that
bootstraps the application. Sprout encourages the use of modules to keep
things organized.
The main of the application may look something like this:
package main
import "github.com/levelfourab/sprout-go"
func main() {
sprout.New("ExampleApp", "v1.0.0").With(
example.Module
).Run()
}
The module can then be defined like this:
package example
import "github.com/levelfourab/sprout-go"
import "go.uber.org/fx"
type Config struct {
Name string `env:"NAME" envDefault:"Test"`
}
var Module = fx.Module(
"example",
fx.Provide(sprout.Config("", &Config{}), fx.Private),
fx.Provide(sprout.Logger("example"), fx.Private),
fx.Invoke(func(cfg *Config, logger *logr.Logger) {
logger.Info("Hello", "name", cfg.Name)
})
)
Development mode
Sprout will act differently if the environment variable DEVELOPMENT is set
to true. This is intended for local development, and will enable things such
as pretty printing logs and disable sending of traces and metrics to an OTLP
backend.
A quick way to enable development mode is to use the DEVELOPMENT=true prefix
when running the application:
DEVELOPMENT=true go run .
As Sprout applications use environment variables for configuration a tool such
as direnv can be used to automatically set variables
when entering the project directory.
A basic .envrc for use with direnv would look like this:
# .envrc
export DEVELOPMENT=true
Entering the project directory will then use this file:
$ cd example
direnv: loading .envrc
direnv: export +DEVELOPMENT
$ go run .
Configuration
Sprout uses environment variables to configure the application. Variables are
read via env into structs.
sprout.Config will create a function that reads the environment variables,
which can be used with fx.Provide.
Example:
type Config struct {
Host string `env:"HOST" envDefault:"localhost"`
Port int `env:"PORT" envDefault:"8080"`
}
var Module = fx.Module(
"example",
fx.Provide(sprout.Config("PREFIX_IF_ANY", &Config{}), fx.Private),
fx.Invoke(func(cfg *Config) {
// Config is now available for use with Fx
})
)
Logging
Sprout provides logging via Zap and
Logr. Sprout will automatically configure
logging based on if the application is running in development or production
mode. In development mode, logs are pretty printed to stderr. In production
mode, logs are formatted as JSON and sent to stderr.
sprout.Logger will create a function that returns a logger, which can be used
with fx.Provide to create a *zap.Logger for a certain module. It is
recommended to use the fx.Private option to make the logger private to the
module.
Example:
var Module = fx.Module(
"example",
fx.Provide(sprout.Logger("example"), fx.Private),
fx.Invoke(func(logger *zap.Logger) {
// Logger is now available for use with Fx
})
)
Variants of sprout.Logger are also available to create a *zap.SugaredLogger
or a logr.Logger.
Example:
fx.Provide(sprout.SugaredLogger("example"), fx.Private)
fx.Provide(sprout.LogrLogger("example"), fx.Private)
Observability
Sprout integrates with OpenTelemetry and will push
data to an OTLP compatible backend such as OpenTelemetry Collector.
This decouples the application from the telemetry backend, allowing for easy
migration to other backends.
The following environment variables are used to configure the OpenTelemetry
integration:
| Variable |
Description |
Default |
OTEL_PROPAGATORS |
The default propagators to use |
tracecontext,baggage |
OTEL_EXPORTER_OTLP_ENDPOINT |
The endpoint to send traces, metrics and logs to |
https://localhost:4317 |
OTEL_EXPORTER_OTLP_TIMEOUT |
The timeout for sending data |
10s |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
Custom endpoint to send traces to, overrides OTEL_EXPORTER_OTLP_ENDPOINT |
https://localhost:4317 |
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT |
Custom timeout for sending traces |
10s |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
Custom endpoint to send metrics to, overrides OTEL_EXPORTER_OTLP_ENDPOINT |
https://localhost:4317 |
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT |
Custom timeout for sending metrics |
10s |
OTEL_TRACING_DEVELOPMENT |
Enable development mode for tracing |
false |
If Sprout is in development mode, the OTLP exporter will be disabled. You can
enable logging of traces using the OTEL_TRACING_DEVELOPMENT environment
variable.
Tracing
Sprout provides an easy way to make a trace.Tracer
available to a module:
var Module = fx.Module(
"example",
fx.Provide(sprout.Tracer("example"), fx.Private),
fx.Invoke(func(tracer trace.Tracer) {
// Tracer is now available for use with Fx
})
)
If the module is internal to the service, you can use sprout.ServiceTracer to
create a tracer based on the service name and version:
var Module = fx.Module(
"internalModule",
fx.Provide(sprout.ServiceTracer(), fx.Private),
fx.Invoke(func(tracer trace.Tracer) {
// Tracer is now available for use with Fx
})
)
Metrics
Sprout provides an easy way to make a metric.Meter
available to a module:
var Module = fx.Module(
"example",
fx.Provide(sprout.Meter("example"), fx.Private),
fx.Invoke(func(meter metric.Meter) {
// Meter is now available for use with Fx
})
)
For modules that are internal to the service, you can use sprout.ServiceMeter
to create a meter based on the service name and version:
var Module = fx.Module(
"internalModule",
fx.Provide(sprout.ServiceMeter(), fx.Private),
fx.Invoke(func(meter metric.Meter) {
// Meter is now available for use with Fx
})
)
Health checks
Sprout will start a HTTP server on port 8088 that exposes a /healthz and
/readyz endpoint. Requests to these will run checks and return a 200 status
code if all checks pass, or a 503 status code if any check fails. The port
that the server listens on can be configured via the HEALTH_SERVER_PORT
environment variable.
Health checks are implemented using Health
with checks being defined via sprout.HealthCheck structs. Checks can then
be added by calling AddLivenessCheck or AddReadinessCheck on the
sprout.Health service.
Example:
var Module = fx.Module(
"example",
fx.Invoke(func(checks sprout.Health) {
checks.AddLivenessCheck(sprout.HealthCheck{
Name: "nameOfCheck",
Check: func(ctx context.Context) error {
// Check health here
return nil
},
})
})
)
Checks can not be added after the application has started. It is recommended to
add checks either using fx.Invoke for simple cases or in a provide function
of a service.
Example with a fictional RemoteService:
var Module = fx.Module(
"healthCheckWithProvide",
fx.Provide(func(lifecycle fx.Lifecycle, checks sprout.HealthChecks) *RemoteService {
service := &RemoteService{
...
}
checks.AddReadinessCheck(sprout.HealthCheck{
Name: "nameOfCheck",
Check: func(ctx context.Context) error {
return service.Ping()
},
})
lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return service.Start()
},
OnStop: func(ctx context.Context) error {
return service.Stop()
},
})
return service
}),
)
License
Sprout is licensed under the MIT License. See LICENSE for the full
license text.