package envelope
import (
"encoding/base64"
"encoding/json"
)
// Envelope is a type that carries a serialized domain payload together with the
// trace propagation context, so a distributed trace stays connected across
// service boundaries regardless of the transport it travels over.
type Envelope struct {
Carrier map[string]string `json:"carrier"` // Propagation carrier filled by the OpenTelemetry propagator: traceparent, tracestate and baggage
Body []byte `json:"body"` // Serialized domain payload; opaque bytes as far as the envelope is concerned
}
// Marshal serializes the Envelope into its wire representation so it can be handed
// to any transport as a single []byte.
func (envelope *Envelope) Marshal() ([]byte, error) {
return json.Marshal(envelope)
}
// Unmarshal is the function that validates and returns an Envelope instance from
// its wire representation, the inverse of Marshal.
func Unmarshal(data []byte) (*Envelope, error) {
var envelope Envelope
err := json.Unmarshal(data, &envelope)
if err != nil {
return nil, err
}
return &envelope, err
}
// MarshalString serializes the Envelope and returns it base64-encoded, so it can be
// handed to a transport that moves text rather than bytes and needs a single safe
// value, such as an HTTP header.
//
// The alphabet is standard base64 with padding (encoding/base64.StdEncoding). It is
// part of the wire contract: producer and consumer must agree on it, so it cannot be
// swapped without breaking every service already speaking this protocol.
//
// The name is deliberately not MarshalText. Implementing encoding.TextMarshaler would
// change what json.Marshal produces for an Envelope, and since Marshal is itself
// json.Marshal, the two would call each other until the stack ran out.
func (envelope *Envelope) MarshalString() (string, error) {
marshaledData, err := envelope.Marshal()
return base64.StdEncoding.EncodeToString(marshaledData), err
}
// UnmarshalString validates and returns an Envelope instance from its base64 string
// representation, the inverse of MarshalString.
//
// The input must use the same standard base64 alphabet with padding that MarshalString
// produces; anything else, and anything that decodes to bytes which are not a valid
// envelope, is reported as an error.
func UnmarshalString(encodedString string) (*Envelope, error) {
var emptyEnvelope Envelope
decodedBytes, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
return &emptyEnvelope, err
}
envelope, unmarshalError := Unmarshal(decodedBytes)
if unmarshalError != nil {
return &emptyEnvelope, unmarshalError
}
return envelope, nil
}
package notification
import (
"errors"
"fmt"
"log/slog"
)
// Level represents the severity of a Notification.
type Level int
// Severity levels, ordered from least to most severe. Info is the zero value,
// so it is the level any Notification built without WithLevel defaults to.
const (
Info Level = iota
Warning
Error
// levelSentinel marks the end of the valid levels: WithLevel validates
// against it, so it must always be the last value in this block. It is
// not a real level — do not use it as one.
levelSentinel
)
// ErrInvalidLevel is returned when a given Level is not one of the defined
// severity levels. Callers can detect it with errors.Is.
var ErrInvalidLevel = errors.New("invalid notification level")
// ErrEmptyProperty is returned when a required Notification property is empty.
// The wrapped message names the offending property. Callers can detect it with
// errors.Is.
var ErrEmptyProperty = errors.New("notification property cannot be empty")
// Notification is a type that defines a message to be delivered to a
// destination with a severity level.
type Notification struct {
destination string // Where the notification is delivered
title string // Notification Title, or subject
message string // Notification content
level Level // Severity level, Info unless set via WithLevel
}
// Option configures a Notification during construction. Options are applied in
// order by NewNotification and may return an error to reject invalid values.
type Option func(*Notification) error
// WithLevel returns an Option that sets the notification severity level. It
// fails with ErrInvalidLevel if level is not one of the defined levels.
func WithLevel(level Level) Option {
return func(notification *Notification) error {
if level < Info || level >= levelSentinel {
return fmt.Errorf("%w: %s", ErrInvalidLevel, level.String())
}
notification.level = level
return nil
}
}
// NewNotification is the function that validates and returns Notification
// instance. Destination, title and message are required and cannot be empty; the
// severity level defaults to Info unless WithLevel is passed.
func NewNotification(destination string, title string, message string, opts ...Option) (Notification, error) {
if destination == "" {
return Notification{}, fmt.Errorf("%w: destination", ErrEmptyProperty)
}
if title == "" {
return Notification{}, fmt.Errorf("%w: title", ErrEmptyProperty)
}
if message == "" {
return Notification{}, fmt.Errorf("%w: message", ErrEmptyProperty)
}
notification := Notification{
destination: destination,
message: message,
title: title,
}
for _, opt := range opts {
if err := opt(¬ification); err != nil {
return Notification{}, err
}
}
return notification, nil
}
// Destination returns where the notification is delivered.
func (notification *Notification) Destination() string {
return notification.destination
}
// Message returns the notification content.
func (notification *Notification) Message() string {
return notification.message
}
// Title returns the notification title.
func (notification *Notification) Title() string {
return notification.title
}
// Level returns the notification severity level.
func (notification *Notification) Level() Level {
return notification.level
}
// String makes Level implement fmt.Stringer, so levels are rendered by
// name instead of as raw integers.
func (level Level) String() string {
switch level {
case Info:
return "info"
case Warning:
return "warning"
case Error:
return "error"
default:
return fmt.Sprintf("unknown(%d)", int(level))
}
}
// LogValue allows to log Notification omitting the message content: only the
// destination and the severity level are logged.
func (notification Notification) LogValue() slog.Value {
return slog.GroupValue(
slog.String("destination", notification.Destination()),
slog.String("title", notification.Title()),
slog.String("level", notification.Level().String()),
)
}
package opentelemetry
import (
"errors"
"net/url"
"os"
)
// ExporterType identifies which exporter family the telemetry pipeline will use.
type ExporterType int
// Exporter types. Stdout is the zero value, used when OTEL_EXPORTER_OTLP_ENDPOINT
// is not defined; OTLP is used when the endpoint is set.
const (
Stdout ExporterType = iota
OTLP
)
// Config is a type that holds the data required to configure OpenTelemetry.
type Config struct {
appName string // The name of the app, sourced from APP_NAME and used as the telemetry service.name
enabled bool // Whether telemetry is active, sourced from ENABLE_TELEMETRY; defaults to false (opt-in)
exporterType ExporterType
exporterURL string
}
// AppName returns the name of the app, used as the telemetry service.name.
func (c *Config) AppName() string {
return c.appName
}
// Enabled returns whether telemetry is active.
func (c *Config) Enabled() bool {
return c.enabled
}
// ExporterType returns the exporter type, derived from OTEL_EXPORTER_OTLP_ENDPOINT.
func (c *Config) ExporterType() ExporterType {
return c.exporterType
}
// ExporterURL returns the OTLP endpoint URL; it is empty when the exporter type is Stdout.
func (c *Config) ExporterURL() string {
return c.exporterURL
}
func validateOTELURL(config *Config) error {
otelExporter, otelExporterVarDefined := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
config.exporterType = Stdout
if otelExporterVarDefined {
parsedURL, parseError := url.ParseRequestURI(otelExporter)
if parseError != nil {
return errors.New("env variable \"OTEL_EXPORTER_OTLP_ENDPOINT\" content is not a valid endpoint")
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return errors.New("env variable \"OTEL_EXPORTER_OTLP_ENDPOINT\" scheme is not valid, only http and https are accepted")
}
if parsedURL.Hostname() == "" {
return errors.New("env variable \"OTEL_EXPORTER_OTLP_ENDPOINT\" hostname is empty")
}
config.exporterType = OTLP
config.exporterURL = otelExporter
}
return nil
}
// NewConfig is the function that validates and returns Config instance
func NewConfig() (*Config, error) {
config := Config{}
config.appName = os.Getenv("APP_NAME")
if config.appName == "" {
return nil, errors.New("env variable \"APP_NAME\" must be defined and have a value")
}
_, otelServiceNameVarDefined := os.LookupEnv("OTEL_SERVICE_NAME")
// APP_NAME is the only accepted source for service.name, so OTEL_SERVICE_NAME must not be set
if otelServiceNameVarDefined {
return nil, errors.New("env variable \"OTEL_SERVICE_NAME\" cannot be defined. APP_NAME will be use to set that value")
}
_, otelResourceAttributesVarDefined := os.LookupEnv("OTEL_RESOURCE_ATTRIBUTES")
// For the time being this variable is forbidden, its values will be managed if required
if otelResourceAttributesVarDefined {
return nil, errors.New("env variable \"OTEL_RESOURCE_ATTRIBUTES\" cannot be defined for the time being")
}
enableFlagValue, enableFlagDefined := os.LookupEnv("ENABLE_TELEMETRY")
// ENABLE_TELEMETRY is the single source of truth for whether telemetry is active.
// It is optional: when unset, enabled stays false (opt-in). Only "true" or "false" are accepted.
if enableFlagDefined {
if enableFlagValue != "true" && enableFlagValue != "false" {
return nil, errors.New("env variable \"ENABLE_TELEMETRY\" valid values are only true or false")
}
config.enabled = enableFlagValue == "true"
}
if config.enabled {
otelURLErr := validateOTELURL(&config)
if otelURLErr != nil {
return nil, otelURLErr
}
}
return &config, nil
}
package rabbitmq
import (
"cmp"
"errors"
"log/slog"
"net/url"
"os"
"strconv"
)
// Config is a type that defines required data for connecting to RabbitMQ server
type Config struct {
host string
port int
user string
password string
ConnectionString string
}
// NewConfig is the function that validates and returns Config instance
func NewConfig() (*Config, error) {
config := new(Config)
// Get host from RABBITMQ_HOST env variable
config.host = cmp.Or(os.Getenv("RABBITMQ_HOST"), "localhost")
// Get user from RABBITMQ_USER env variable
config.user = cmp.Or(os.Getenv("RABBITMQ_USER"), "guest")
// Get password from RABBITMQ_PASSWORD env variable
config.password = cmp.Or(os.Getenv("RABBITMQ_PASSWORD"), "guest")
// Get port from RABBITMQ_PORT env variable and validate its value
var portAtoiErr error
config.port, portAtoiErr = strconv.Atoi(cmp.Or(os.Getenv("RABBITMQ_PORT"), "5672"))
if portAtoiErr != nil {
return nil, portAtoiErr
}
if config.port <= 0 || config.port >= 65536 {
return nil, errors.New("RabbitMQ port value must be between 1 and 65535")
}
connectionURL := url.URL{
Scheme: "amqp",
User: url.UserPassword(config.user, config.password),
Host: config.host + ":" + strconv.Itoa(config.port),
Path: "/",
}
config.ConnectionString = connectionURL.String()
return config, nil
}
// LogValue allows to log conection URL masking password value
func (config Config) LogValue() slog.Value {
connectionURL := url.URL{
Scheme: "amqp",
User: url.UserPassword(config.user, "xxxxx"),
Host: config.host + ":" + strconv.Itoa(config.port),
Path: "/",
}
return slog.GroupValue(
slog.String("url", connectionURL.String()),
)
}
package redis
import (
"cmp"
"errors"
"log/slog"
"os"
"strconv"
)
// Config is a type that defines required data for connecting to Redis server
type Config struct {
Host string
Port int
Password string
Database int
}
// NewConfig is the function that validates and returns Config instance
func NewConfig() (*Config, error) {
config := Config{}
// Get host from REDIS_HOST env variable
config.Host = cmp.Or(os.Getenv("REDIS_HOST"), "localhost")
// Get port from REDIS_PORT env variable
var portAtoiErr error
config.Port, portAtoiErr = strconv.Atoi(cmp.Or(os.Getenv("REDIS_PORT"), "6379"))
if portAtoiErr != nil {
return nil, portAtoiErr
}
if config.Port <= 0 || config.Port >= 65536 {
return nil, errors.New("Redis port value must be between 1 and 65535")
}
// Get database from REDIS_DATABASE env variable
var databaseAtoiErr error
config.Database, databaseAtoiErr = strconv.Atoi(cmp.Or(os.Getenv("REDIS_DATABASE"), "0"))
if databaseAtoiErr != nil {
return nil, databaseAtoiErr
}
if config.Database < 0 {
return nil, errors.New("Redis database value must be a positive integer")
}
// Get password from REDIS_PASSWORD env variable
config.Password = cmp.Or(os.Getenv("REDIS_PASSWORD"), "")
return &config, nil
}
// LogValue allows to log Config masking password value
func (config Config) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("host", config.Host),
slog.Int("port", config.Port),
slog.Int("database", config.Database),
}
if config.Password != "" {
attrs = append(attrs, slog.String("password", "*****"))
}
return slog.GroupValue(attrs...)
}
package slog
import (
"cmp"
"errors"
"fmt"
formerslog "log/slog"
"os"
)
// Config is a type that defines required data for defining slog parameters
type Config struct {
DefaultLevel formerslog.Level // Specifies log default level, Info for example
Format string // Log format JSON or plain
AddSource bool // Adds file:line to logs
AppName string // The name of the app
}
// NewConfig is the function that validates and returns Config instance
func NewConfig() (*Config, error) {
config := Config{}
// Get log level from SLOG_LEVEL `env`` variable
defaultLevel := cmp.Or(os.Getenv("SLOG_LEVEL"), "Info")
// define config.DefaultLevel from defaultLevel value
switch defaultLevel {
case "Debug":
config.DefaultLevel = formerslog.LevelDebug
case "Info":
config.DefaultLevel = formerslog.LevelInfo
case "Warn":
config.DefaultLevel = formerslog.LevelWarn
case "Error":
config.DefaultLevel = formerslog.LevelError
default:
return nil, fmt.Errorf("log level defined by `SLOG_LEVEL` variable only accepts the following values: \"Debug\", \"Info\", \"Warn\" or \"Error\". \"%s\" is not a valid value.", defaultLevel)
}
// Get format from SLOG_FORMAT env variable, default value is JSON
config.Format = cmp.Or(os.Getenv("SLOG_FORMAT"), "JSON")
if config.Format != "JSON" && config.Format != "plain" {
return nil, fmt.Errorf("log format defined by `SLOG_FORMAT` variable only accepts the values \"JSON\" or \"plain\", \"%s\" is not a valid value", config.Format)
}
// Get AddSource value from SLOG_ADD_SOURCE `env` variable
addSource := cmp.Or(os.Getenv("SLOG_ADD_SOURCE"), "true")
if addSource != "false" && addSource != "true" {
return nil, fmt.Errorf("add log source property defined by `SLOG_ADD_SOURCE` variable only accepts the values \"true\" or \"false\", \"%s\" is not a valid value", addSource)
}
config.AddSource = addSource == "true"
config.AppName = cmp.Or(os.Getenv("APP_NAME"), "")
if config.AppName == "" {
return nil, errors.New("env variable \"APP_NAME\" must be defined and have a value")
}
return &config, nil
}
package smtp
import (
"cmp"
"errors"
"fmt"
"log/slog"
"net/mail"
"os"
"strconv"
)
// Config is a type that defines required data for connecting to a SMTP server
type Config struct {
from string
host string // SMTP server domain
port int // SMTP server port
username string // SMTP authentication username
password string // SMTP authentication password
validateTLS bool // Whether to validate TLS certificates
}
// NewConfig is the function that validates and returns Config instance
func NewConfig() (*Config, error) {
config := Config{}
// Check if all required environment variables are defined
requiredEnvVariables := []string{"SMTP_FROM", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD"}
for _, requiredEnvVariable := range requiredEnvVariables {
if _, envVariableFound := os.LookupEnv(requiredEnvVariable); !envVariableFound {
errorString := fmt.Sprintf("env variable \"%s\" must be set, cannot load smtp config", requiredEnvVariable)
return nil, errors.New(errorString)
}
}
// Parse SMTP port from string to integer
port, portAtoiError := strconv.Atoi(os.Getenv("SMTP_PORT"))
if portAtoiError != nil {
return nil, errors.New("failed to parse \"SMTP_PORT\" value")
}
if port <= 0 || port >= 65536 {
return nil, errors.New("SMTP port value must be between 1 and 65535")
} else {
config.port = port
}
if _, err := mail.ParseAddress(os.Getenv("SMTP_FROM")); err != nil {
return nil, errors.New("\"SMTP_FROM\" is not a valid email address")
}
// Load SMTP configuration from environment variables
config.host = os.Getenv("SMTP_HOST")
config.from = os.Getenv("SMTP_FROM")
config.username = os.Getenv("SMTP_USERNAME")
config.password = os.Getenv("SMTP_PASSWORD")
// Check SMTP TLS validation setting (defaults to true if not specified)
config.validateTLS = cmp.Or(os.Getenv("SMTP_VALIDATE_TLS"), "true") == "true"
return &config, nil
}
// From returns the sender email address
func (config *Config) From() string {
return config.from
}
// Host returns the SMTP server hostname
func (config *Config) Host() string {
return config.host
}
// Port returns the SMTP server port
func (config *Config) Port() int {
return config.port
}
// Address returns the SMTP server address in "host:port" form
func (config *Config) Address() string {
return fmt.Sprintf("%s:%d", config.host, config.port)
}
// Username returns the SMTP authentication username
func (config *Config) Username() string {
return config.username
}
// Password returns the SMTP authentication password
func (config *Config) Password() string {
return config.password
}
// ValidateTLS returns whether TLS certificates are validated
func (config *Config) ValidateTLS() bool {
return config.validateTLS
}
// LogValue allows to log Config masking password value
func (config Config) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("from", config.from),
slog.String("address", config.Address()),
slog.String("user", config.username),
slog.String("password", "*****"),
slog.Bool("validate_tls", config.validateTLS),
}
return slog.GroupValue(attrs...)
}