cf_resend

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

caerus-framework-resend

CI codecov License

Caerus Framework Resend Component. A thin framework wrapper around the resend-go SDK so components and apps can send email without importing the SDK directly: framework-owned lifecycle, configuration (file + env + flags), live config reload with last-good semantics, logging through the framework logs component, and observability health/metrics.

Wiring

Two wiring shapes are supported. Prefer the app-owned shape (demoapp golden path): main declares only the chassis (resend alongside postgres / valkey) and the app class; product machinery that sends email lives under the app and resolves resend as a peer at Init. Use the simple main-level shape for one-off binaries.

App-owned consumer (golden — demoapp pattern)

main declares resend as chassis and runs the app class; it never touches resend itself:

fw := cf.New(&cf.FrameworkOptions{
	Logs: &cf.LogsSettings{Format: "json", Level: "info", ConfigSource: "logs"},
	Observability: &cf.ObservabilitySettings{Address: ":9090", ConfigSource: "observability"},
	Components: []cf.CaerusComponent{
		cf_postgres.New(cf_postgres.WithConfigSource("postgresql", "config/postgresql.json")),
		cf_resend.New(cf_resend.WithConfigSource("resend", "config/resend.json")),
		app.New(app.Options{}),
	},
})
if err := fw.RunWithSignals(context.Background()); err != nil {
	log.Fatal(err)
}

The app resolves the resend component pointer once at Init (never a client snapshot), declares it in GetDependencies, and calls Send per use:

type App struct {
	email *cf_resend.CFResend
}

func (a *App) GetDependencies() []string {
	return []string{cf_resend.ComponentName} // + logs, chassis peers
}

func (a *App) Init(ctx context.Context, fw *cf.CaerusFramework) error {
	email, ok := cf.Get[*cf_resend.CFResend](fw)
	if !ok {
		return errors.New("app: resend component missing")
	}
	a.email = email
	return nil
}
Simple main-level wiring

For a one-off binary, register the components directly and use cf.MustGet to reach the component:

fw := cf.New()

logs := cf_logs.New(cf_logs.WithWriter(os.Stdout))
resend := cf_resend.New(cf_resend.WithConfigSource("resend", "config/resend.json"))
fw.AddComponent(logs)
fw.AddComponent(resend) // GetDependencies() -> [logs configuration]

In both shapes the component is cf.ConfigSourceRegistrar-self-sufficient: WithConfigSource registers the Source[ResendConfig] with the configuration component during argv absorption, so main never touches os.Getenv/ParseFlags. The --resend path flag and per-field flags come from the source declaration.

Sending

Send fills From from the configured from_address when the request leaves it empty, requires at least one recipient, and honors the context end-to-end (Emails.SendWithContext). The app resolves the component once at Init (see Wiring above) and sends per use:

// in the app's Init — store the component pointer
a.email, _ = cf.Get[*cf_resend.CFResend](fw)

// per use
resp, err := a.email.Send(ctx, &resend.SendEmailRequest{
	To:      []string{"user@example.com"},
	Subject: "Welcome",
	Html:    "<p>Hi!</p>",
})

Peers resolve the component once at Init (declare cf_resend.ComponentName in GetDependencies) and call Send/Client() per use — never snapshot the client, since config reload swaps it.

Options

Option Description
WithConfig(ResendConfig) static config snapshot; non-zero fields override option-set defaults
WithConfigSource(name, path, …) bind a configuration source for Init + OnConfigReload; the module registers the Source[ResendConfig] itself (declares configuration dep)
WithAPIKey(key) set the Resend API key directly (tests, embedded use)
WithFromAddress(from) default sender address
WithBaseURL(url) override the Resend API endpoint (self-hosted / test stub)
WithTimeout(d) per-send HTTP timeout (default 10s)
WithHTTPClient(*http.Client) override the HTTP client (stub RoundTripper in tests)
WithName(name) custom component name for multiple instances (default "resend")
WithLogger(*slog.Logger) explicit logger override; defaults to the framework logs component's logger (re-delivered on logs Reconfigure), falling back to slog.Default()

Configuration

Load ResendConfig through the configuration component. The default EnvPrefix is RESEND_ (from the source name); env tags map RESEND_API_KEY, RESEND_FROM_ADDRESS, RESEND_BASE_URL, RESEND_TIMEOUT_SEC.

{
  "api_key": "…",
  "from_address": "noreply@example.com"
}

Files are canonical in Kubernetes, including the API key — mount a Secret/ConfigMap and let fsnotify + OnConfigReload rotate the client without a restart. api_key is tagged secret:"redact": reload logs api_key_set, never the key. Do not log the config struct. On reload failure the previous client stays live (last-good). Resend is a stateless HTTP wrapper, so Client() and Send keep working through a swap.

Health reports initialized/uninitialized (Resend has no liveness endpoint; send failures surface per call). Metrics emits the following while initialized, nil before Init/after Shutdown:

Metric Type Labels
resend_info gauge 1 component, from, base_url
resend_config_reloads_total counter component
resend_emails_sent_total counter component, from
resend_emails_failed_total counter component, from, error_code
resend_send_duration_seconds_sum counter component, from
resend_send_duration_seconds_count counter component, from

resend_info is a snapshot descriptor (the "component is initialized" marker); its labels always reflect the live configured sender identity. Error codes are the real HTTP statuses (e.g. 429, 422, 500), or network for transport errors — recorded at the transport layer because the resend SDK swallows status codes in its errors. Average send latency is resend_send_duration_seconds_sum / resend_send_duration_seconds_count; the send error rate is rate(resend_emails_failed_total[5m]) / rate(resend_emails_sent_total[5m]) grouped by error_code.

The from label on the traffic counters is the actual sender of each email — the resolved req.From, or the configured default when the request leaves it empty. Sends are bucketed per sender at send time, so overrides and a from_address change never relabel history. Sends made directly through Client().Emails (bypassing Send) are attributed to from="unknown". Before any traffic, the counters appear at zero for the configured default sender.

Tests

Unit tests cover config layering, the Init contract, reload last-good, and health/metrics transitions using a stub http.RoundTripper — no external service.

License

Apache License 2.0 — see LICENSE.

Documentation

Index

Constants

View Source
const (
	// ComponentName is the framework component name for the resend component.
	// It is the identifier other components use in GetDependencies to require
	// resend.
	ComponentName = "resend"

	// ComponentStage is the stage data-layer components initialize in. It is
	// not a built-in bootstrap stage; AddComponent registers it automatically
	// the first time a component declares it.
	ComponentStage = cf.Stage("data")
)

Variables

This section is empty.

Functions

This section is empty.

Types

type CFResend

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

CFResend is the caerus-framework-resend component. It wraps the Resend SDK client and hands out live accessors (Client, From) to peers.

func New

func New(opts ...Option) *CFResend

New creates a resend component. The client is built at Init, not here.

func (*CFResend) BaseURL

func (c *CFResend) BaseURL() string

BaseURL returns the configured Resend API endpoint (empty = the SDK default).

func (*CFResend) Client

func (c *CFResend) Client() *resend.Client

Client returns the live Resend SDK client. It is non-nil after a successful Init and nil before Init or after Shutdown. Call it per use rather than caching the pointer; the component swaps the client on config reload.

func (*CFResend) From

func (c *CFResend) From() string

From returns the configured default sender address (may be empty; then Send calls must carry their own From).

func (*CFResend) GetDependencies

func (c *CFResend) GetDependencies() []string

GetDependencies implements cf.Dependencies. The component logs through the framework logs component, and depends on configuration when WithConfigSource is set.

func (*CFResend) GetInitOrderStage

func (c *CFResend) GetInitOrderStage() cf.Stage

GetInitOrderStage implements cf.CaerusComponent.

func (*CFResend) Health

func (c *CFResend) Health(ctx context.Context) error

Health implements cf.HealthProvider. Resend exposes no liveness endpoint, so health reflects that a client is initialized (nil before Init or after Shutdown). Connectivity is verified on each Send error.

func (*CFResend) Init

func (c *CFResend) Init(ctx context.Context, fw *cf.CaerusFramework) error

Init implements cf.CaerusComponent. It builds the Resend client from the option-set or configuration-source credentials. An empty API key fails startup (fail-fast) so a misconfigured mailer never silently swallows sends.

func (*CFResend) Metrics

func (c *CFResend) Metrics() []cf_observability.Metric

Metrics implements cf_observability.MetricsProvider. It reports the resend client's identity, per-sender send traffic and latency; before Init or after Shutdown it returns nil, so the observability component skips it (lazy pickup).

resend_info is a snapshot descriptor gauge (value 1) carrying the live configured sender identity; it is the "component is initialized" marker. Traffic counters are bucketed by the actual sender of each email (the resolved req.From, or the configured default), so the from label is accurate per send and a from_address change never relabels history.

func (*CFResend) Name

func (c *CFResend) Name() string

Name implements cf.CaerusComponent. Returns the custom name set via WithName, or the default ComponentName ("resend") if no custom name was set.

func (*CFResend) OnConfigReload

func (c *CFResend) OnConfigReload(source string, cfg any)

OnConfigReload implements cf.ConfigReloader. It rebuilds the client from the bound configuration source. The fresh value is delivered as cfg but the client is rebuilt from the source so the translation stays in one place. On failure the previous client is kept.

func (*CFResend) RegisterConfigSources

func (c *CFResend) RegisterConfigSources(conf any) error

RegisterConfigSources implements cf.ConfigSourceRegistrar. The framework calls it during argv absorption; it registers this component's configuration source (name, path, env prefix, format, Owner) with the configuration component. No-op when no source is bound.

func (*CFResend) Send

Send sends an email through Resend. When req.From is empty the component's configured FromAddress is used; an empty final From is an error. The context is honored end-to-end (the SDK's SendWithContext). The caller's request is not mutated. Send traffic metrics are attributed to the resolved sender via the context; sends made directly through Client().Emails (bypassing Send) are attributed to "unknown".

func (*CFResend) Shutdown

func (c *CFResend) Shutdown(ctx context.Context) error

Shutdown implements cf.CaerusComponent. It unsubscribes the logs subscription and releases the client. Further use of Client() after shutdown returns nil.

type Option

type Option func(*options)

Option configures the resend component at construction time.

func WithAPIKey

func WithAPIKey(apiKey string) Option

WithAPIKey sets the Resend API key directly (tests, embedded use). Prefer WithConfigSource for production so the key rotates via config reload.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL overrides the Resend API endpoint. Empty uses the SDK default.

func WithConfig

func WithConfig(cfg ResendConfig) Option

WithConfig sets a static configuration snapshot. Non-zero fields of cfg override the values set by the convenience options. Prefer WithConfigSource when using caerus-framework-configuration with hot-reload.

func WithConfigSource

func WithConfigSource(name, path string, opts ...SourceOption) Option

WithConfigSource binds this component to a named configuration source and registers that source with the configuration component (via the framework's ConfigSourceRegistrar pass during argv absorption). The module owns the Source: the config type, the default EnvPrefix and its Owner (Name(), so named instances reload correctly). main only points the instance at where the config lives.

cf_resend.New(cf_resend.WithConfigSource("resend", "config/resend.json"))
cf_resend.New(cf_resend.WithConfigSource("mailer", "/etc/app/mailer.yaml",
    cf_resend.WithSourceFormat(cf_configuration.FormatYAML)))

A path of "" registers an env-only (fileless) source when the EnvPrefix is non-empty. The path CLI override stays --<source-name> (ParseFlags). Declares a dependency on "configuration".

func WithFromAddress

func WithFromAddress(from string) Option

WithFromAddress sets the default sender address.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient overrides the HTTP client used for Resend calls. Useful for tests (a stub RoundTripper) and for sharing a process-wide transport. The component does not close a client it did not create.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger overrides the logger used for component diagnostics. By default the component logs through the framework logs component (declared in GetDependencies); WithLogger is an explicit override for tests and embedded use and wins over the framework logger. slog.Default() remains the fallback only when neither is available.

func WithName

func WithName(name string) Option

WithName sets a custom component name, allowing multiple resend instances in the same process. The default name is "resend" (ComponentName). Use this when you need several mail senders (e.g. branded vs system) in one binary. Retrieve named instances with GetByName[*CFResend](fw, "mailer").

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-send HTTP timeout (default 10s).

type ResendConfig

type ResendConfig struct {
	// APIKey is the Resend API key (resend.com, or a self-hosted instance).
	APIKey string `json:"api_key" yaml:"api_key" env:"API_KEY" secret:"redact"`
	// FromAddress is the default sender address (e.g. "noreply@example.com").
	// Send leaves it to the caller when the request carries its own From.
	FromAddress string `json:"from_address" yaml:"from_address" env:"FROM_ADDRESS"`
	// BaseURL overrides the Resend API endpoint. Empty uses the SDK default.
	// Useful for self-hosted instances or tests that stub the API.
	BaseURL string `json:"base_url,omitempty" yaml:"base_url,omitempty" env:"BASE_URL"`
	// TimeoutSec bounds each Resend HTTP call (default 10s).
	TimeoutSec float64 `json:"timeout_sec,omitempty" yaml:"timeout_sec,omitempty" env:"TIMEOUT_SEC"`
}

ResendConfig is the file/env-drivable configuration. Load it through the configuration component (caerus-framework-configuration) and pass it via WithConfigSource; both JSON and YAML tags are provided.

type SourceOption

type SourceOption func(*sourceOptions)

SourceOption configures the self-registered configuration source created by WithConfigSource.

func WithSourceEnvPrefix

func WithSourceEnvPrefix(prefix string) SourceOption

WithSourceEnvPrefix sets the environment overlay prefix for the source (default: the uppercase source name with "-" replaced by "_", plus "_"). An empty prefix disables env overlay.

func WithSourceFormat

func WithSourceFormat(f cf_configuration.Format) SourceOption

WithSourceFormat forces the file format instead of inferring it from the path extension (".yaml"/".yml" → YAML; anything else JSON).

Jump to

Keyboard shortcuts

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