sprout

package module
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Jan 5, 2024 License: MIT Imports: 12 Imported by: 0

README

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
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
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
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
  }),
)

Working with the code

Pre-commit hooks

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.

Commit messages

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.

Code style

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.

Linting

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
Running tests

Ginkgo is used for testing. Tests can be run via go test but the ginkgo CLI provides an improved experience:

ginkgo run ./...

License

Sprout is licensed under the MIT License. See LICENSE for the full license text.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindConfig

func BindConfig(prefix string, value any) any

BindConfig is an on-demand version of Config. It will read configuration from the environment and bind them to the specified struct.

func Config

func Config[T any](prefix string, value T) any

Config will read configuration from the environment and provide the specified type to the application.

Example:

type Config struct {
	Host string `env:"HOST" envDefault:"localhost"`
	Port int    `env:"PORT" envDefault:"8080"`
}

sprout.New("my-service", "1.0.0").With(
	fx.Provide(sprout.Config("HTTP", Config{})),
	fx.Invoke(func(config Config) {
		// ...
	}),
).Run()

func Logger

func Logger(name ...string) any

Logger creates a function that can be used to create a logger with a name. Can be used with fx.Decorate or fx.Provide.

It is mostly used when creating a module, to supply a logger with a name that is specific to the module.

Example:

var Module = fx.Module(
	"example",
	fx.Decorate(sprout.Logger("name", "of", "logger")),
	fx.Invoke(func(logger *zap.Logger) {
		// ...
	}),
)

func LogrLogger added in v0.2.0

func LogrLogger(name ...string) any

LogrLogger creates a function that can be used to create a logr logger with a name. Can be used with fx.Decorate or fx.Provide.

It is mostly used when creating a module, to supply a logr logger with a name that is specific to the module.

Example:

var Module = fx.Module(
	"example",
	fx.Decorate(sprout.LogrLogger("name", "of", "logger")),
	fx.Invoke(func(logger logr.Logger) {
		// ...
	}),
)

func Meter

func Meter(name string, opts ...metric.MeterOption) any

Meter returns a function that can be used with fx.Provide to make a meter available to the application.

func ServiceMeter added in v0.3.0

func ServiceMeter(opts ...metric.MeterOption) any

ServiceMeter returns a function that can be used with fx.Provide to make a meter available to the application. The meter will use the name and version of the service.

func ServiceTracer added in v0.3.0

func ServiceTracer(opts ...trace.TracerOption) any

ServiceTracer returns a function that can be used with fx.Provide to make a tracer available to the application. The tracer will use the name and version of the service.

func SugaredLogger added in v0.2.0

func SugaredLogger(name ...string) any

SugaredLogger creates a function that can be used to create a sugared logger with a name. Can be used with fx.Decorate or fx.Provide.

It is mostly used when creating a module, to supply a sugared logger with a name that is specific to the module.

Example:

var Module = fx.Module(
	"example",
	fx.Decorate(sprout.SugaredLogger("name", "of", "logger")),
	fx.Invoke(func(logger *zap.SugaredLogger) {
		// ...
	}),
)

func Tracer

func Tracer(name string, opts ...trace.TracerOption) any

Tracer returns a function that can be used with fx.Provide to make a tracer available to the application.

Types

type Health added in v0.3.0

type Health = health.Checks

type HealthCheck

type HealthCheck = health.Check

type Sprout

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

func New

func New(name string, version string) *Sprout

New creates a new Sprout application. The name and version will be used to identify the application in logs, traces and metrics.

func (*Sprout) With

func (s *Sprout) With(options ...fx.Option) *fx.App

With lets you specify Fx options to be used when creating the application.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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