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.
- π 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
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)
})
)
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 .
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
})
)
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)
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 | |
OTEL_EXPORTER_OTLP_TIMEOUT |
The timeout in seconds for sending data | 10 |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
Custom endpoint to send traces to, overrides OTEL_EXPORTER_OTLP_ENDPOINT |
|
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT |
Custom timeout in seconds for sending traces | 10 |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
Custom endpoint to send metrics to, overrides OTEL_EXPORTER_OTLP_ENDPOINT |
|
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT |
Custom timeout in seconds for sending metrics | 10 |
OTEL_METRIC_EXPORT_INTERVAL |
The interval in seconds to export metrics | 60 |
OTEL_METRIC_EXPORT_TIMEOUT |
The timeout in seconds for exporting metrics | 30 |
OTEL_TRACING_LOG |
Enable logging mode for tracing | false |
OTLP exporting is disabled by default. You can enable logging of traces which
can be useful for development by setting the OTEL_TRACING_LOG
environment
variable to true
.
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
})
)
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
})
)
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
}),
)
pre-commit is used to run various checks on the code before it is committed. To install the hooks, run:
pre-commit install -t pre-commit -t pre-commit-msg
Commits will fail if any of the checks fail. If files are modified during the checks, such as for formatting, you will need to add the modified files to the commit again.
Conventional Commits is used for commit messages. This allows for automatic generation of changelogs and version numbers. commitlint is used to enforce the commit message format as a pre-commit hook.
gofmt
and goimports is
used for code formatting. Code formatting will run automatically as part of the
pre-commit hooks.
In addition to this EditorConfig is used to ensure consistent code style across editors.
golangci-lint is used for linting. Linters will run automatically as part of the pre-commit hooks. To run the linters manually:
golangci-lint run
Ginkgo is used for testing. Tests can be run
via go test
but the ginkgo
CLI provides an improved experience:
ginkgo run ./...
Sprout is licensed under the MIT License. See LICENSE for the full license text.