api calls
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// Package config loads runtime settings from the environment.
|
||||
//
|
||||
// Environment variables rather than a config file, for two reasons: the DSN
|
||||
// differs per machine and you are syncing the repo across two, and a file with
|
||||
// credentials in it is one `git add .` away from being public. The bot uses
|
||||
// config.json because its token is the only secret; the backend holds database
|
||||
// credentials and key material.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Addr is the listen address. Bind to localhost in production and put a
|
||||
// reverse proxy in front for TLS.
|
||||
Addr string
|
||||
|
||||
// DSN for Postgres. Required.
|
||||
DSN string
|
||||
|
||||
// MaxConns caps the pgx pool. Postgres forks a backend per connection, so
|
||||
// this is a real resource, not a client-side nicety.
|
||||
MaxConns int32
|
||||
|
||||
// RedisAddr enables the shared rate limiter. Empty falls back to an
|
||||
// in-process limiter, which is correct for a single instance and wrong for
|
||||
// several - each would keep its own counters.
|
||||
RedisAddr string
|
||||
RedisDB int
|
||||
|
||||
// EndUserHeader carries the caller's identifier for whoever triggered the
|
||||
// request, for per-user rate limiting. Empty disables it.
|
||||
EndUserHeader string
|
||||
|
||||
LogLevel slog.Level
|
||||
LogJSON bool
|
||||
|
||||
ReadHeaderTimeout time.Duration
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
c := Config{
|
||||
Addr: env("HAMMY_ADDR", "127.0.0.1:8080"),
|
||||
DSN: os.Getenv("HAMMY_DSN"),
|
||||
RedisAddr: os.Getenv("HAMMY_REDIS_ADDR"),
|
||||
EndUserHeader: env("HAMMY_ENDUSER_HEADER", "X-Hammy-User"),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ShutdownTimeout: 20 * time.Second,
|
||||
LogJSON: boolEnv("HAMMY_LOG_JSON", false),
|
||||
}
|
||||
|
||||
if c.DSN == "" {
|
||||
return Config{}, errors.New("config: HAMMY_DSN is not set")
|
||||
}
|
||||
|
||||
n, err := intEnv("HAMMY_MAX_CONNS", 10)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
c.MaxConns = int32(n)
|
||||
|
||||
if c.RedisDB, err = intEnv("HAMMY_REDIS_DB", 0); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
if c.LogLevel, err = levelEnv("HAMMY_LOG_LEVEL", slog.LevelInfo); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
if c.ReadHeaderTimeout, err = durEnv("HAMMY_READ_HEADER_TIMEOUT", c.ReadHeaderTimeout); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
if c.ShutdownTimeout, err = durEnv("HAMMY_SHUTDOWN_TIMEOUT", c.ShutdownTimeout); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Redacted is safe to log at startup. The DSN carries a password.
|
||||
func (c Config) Redacted() string {
|
||||
redis := c.RedisAddr
|
||||
if redis == "" {
|
||||
redis = "(in-process limiter)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("addr=%s db=%s redis=%s max_conns=%d level=%s",
|
||||
c.Addr, redactDSN(c.DSN), redis, c.MaxConns, c.LogLevel)
|
||||
}
|
||||
|
||||
// redactDSN keeps the shape of the connection string and drops the password.
|
||||
func redactDSN(dsn string) string {
|
||||
at := strings.LastIndex(dsn, "@")
|
||||
if at < 0 {
|
||||
return "(set)"
|
||||
}
|
||||
|
||||
scheme := strings.Index(dsn, "://")
|
||||
if scheme < 0 {
|
||||
return "(set)"
|
||||
}
|
||||
|
||||
creds := dsn[scheme+3 : at]
|
||||
|
||||
user := creds
|
||||
if colon := strings.Index(creds, ":"); colon >= 0 {
|
||||
user = creds[:colon]
|
||||
}
|
||||
|
||||
return dsn[:scheme+3] + user + ":***" + dsn[at:]
|
||||
}
|
||||
|
||||
func env(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
func intEnv(key string, def int) (int, error) {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("config: %s: %w", key, err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func boolEnv(key string, def bool) bool {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func durEnv(key string, def time.Duration) (time.Duration, error) {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("config: %s: %w", key, err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func levelEnv(key string, def slog.Level) (slog.Level, error) {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
var l slog.Level
|
||||
if err := l.UnmarshalText([]byte(v)); err != nil {
|
||||
return 0, fmt.Errorf("config: %s: %w", key, err)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
Reference in New Issue
Block a user