Skip to main content

Typed configuration & secrets facade for AmpyFin (layering, validation, secrets, control-plane)

Project description

๐Ÿš€ ampy-config

Typed Configuration & Secrets Faรงade for AmpyFin

Python License Tests PyPI Code style: black

๐ŸŽฏ Single, safe source of truth for configuration and secrets across AmpyFin services
๐Ÿ”— Built to integrate with ampy-bus (control plane over NATS/JetStream) and ampy-proto (payload contracts)

๐Ÿ“– Documentation โ€ข ๐Ÿš€ Quick Start โ€ข ๐Ÿ”ง Usage โ€ข ๐Ÿค Contributing


๐Ÿ“‹ Table of Contents


๐ŸŽฏ Why this exists (the problem)

Without a unified configuration layer, distributed trading systems tend to develop:

โš ๏ธ Common Issues:

  • ENV/YAML sprawl โ†’ drift, surprises, outages
  • Secret handling risks โ†’ credentials in logs, brittle rotations, no redaction
  • Non-reproducibility โ†’ can't reconstruct exactly which parameters were live for a given trade/run
  • Inconsistent runtime behavior โ†’ some services reload, others require restarts

ampy-config provides a single, typed, validated, observable configuration view with clean secret indirection and a runtime control plane for safe updates.


โœจ Highlights (what you get)

Feature Description
๐Ÿ” Typed schema + validation JSON Schema + semantic cross-field checks
๐Ÿ“š Layering & precedence defaults โ†’ environment profile โ†’ overlays โ†’ ENV allowlist โ†’ runtime overrides
๐Ÿ” Secret indirection secret://โ€ฆ, aws-sm://โ€ฆ, gcp-sm://โ€ฆ with caching, rotation, and universal redaction
๐ŸŽฎ Control plane for updates config_preview โ†’ config_apply โ†’ config_applied events on NATS (JetStream)
๐Ÿ“Š Auditability & observability provenance for each key; logs/metrics/traces (no secrets)
๐ŸŒ Language-agnostic produces plain YAML effective config for Python, Go, C++, etc.

๐Ÿš€ Quick Start

โš ๏ธ Prerequisites - NATS Server Required

ampy-config requires a running NATS server for configuration management:

# Start NATS with JetStream (required)
docker run --rm -d --name nats -p 4222:4222 nats:2.10 -js

# Or install NATS server locally
go install github.com/nats-io/nats-server/v2@latest
nats-server -js

Verify NATS is running:

# Test connection
nats --server "nats://localhost:4222" server info

๐Ÿšจ Common Issue: If you get nats: no servers available for connection error, NATS server is not running. Start it with the command above.

๐Ÿš€ Install

๐Ÿ Python / PyPI

pip install ampy-config

Developer mode (local repo):

pip install -e .

๐Ÿน Go Client

Library:

go get github.com/AmpyFin/ampy-config/go/ampyconfig@v1.1.5

Binaries:

cd go/ampyconfig
make     # builds bin/ampyconfig-{ops,agent,listener}

๐Ÿ“ฆ Available on pkg.go.dev

๐Ÿ”ง Optional secret backends

Backend Install Command Use Case
๐Ÿ” HashiCorp Vault pip install hvac Enterprise secret management
โ˜๏ธ AWS Secrets Manager pip install boto3 AWS-native secret storage
๐ŸŒ GCP Secret Manager pip install google-cloud-secret-manager Google Cloud secret storage

๐Ÿ’ก Tip: You do not need to sign up for all of these. Choose one or more real backends for your deployment; the library gracefully falls back to a local JSON file in development.


๐ŸŽฏ Basic Usage Examples

๐Ÿ Python Quick Start

import asyncio
from ampy_config.layering import build_effective_config

# Build effective configuration
cfg, _ = build_effective_config(
    schema_path="schema/ampy-config.schema.json",
    defaults_path="config/defaults.yaml",
    profile_yaml="examples/dev.yaml",
    overlays=[],
    service_overrides=[],
    env_allowlist_path="env_allowlist.txt",
    env_file=None,
    runtime_overrides_path="runtime/overrides.yaml",
)

# Get configuration values
nats_url = cfg["bus"]["nats_url"]
topic_prefix = cfg["bus"]["topic_prefix"]
risk_limit = cfg["oms"]["risk"]["max_order_notional_usd"]

print(f"NATS URL: {nats_url}")
print(f"Topic Prefix: {topic_prefix}")
print(f"Risk Limit: {risk_limit}")

๐Ÿน Go Quick Start

package main

import (
    "fmt"
    "log"
    
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
    "github.com/nats-io/nats.go"
)

func main() {
    // Connect to NATS
    nc, err := nats.Connect("nats://localhost:4222")
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Close()
    
    // Create ampy-config client
    client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")
    
    // Get configuration values
    natsURL, err := client.Get("bus.nats_url")
    if err != nil {
        log.Fatal(err)
    }
    
    topicPrefix, err := client.Get("bus.topic_prefix")
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("NATS URL: %s\n", natsURL)
    fmt.Printf("Topic Prefix: %s\n", topicPrefix)
}

๐Ÿงช Test Your Setup

Python validation test:

python -c "
from ampy_config.layering import build_effective_config
try:
    cfg, _ = build_effective_config(
        'schema/ampy-config.schema.json',
        'config/defaults.yaml',
        'examples/dev.yaml',
        [], [], 'env_allowlist.txt', None, 'runtime/overrides.yaml'
    )
    print('โœ… ampy-config working correctly!')
    print(f'Bus config: {cfg[\"bus\"]}')
except Exception as e:
    print(f'โŒ Error: {e}')
"

Go validation test:

go run - << 'EOF'
package main

import (
    "fmt"
    "log"
    
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
)

func main() {
    client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")
    
    // Test getting a value
    value, err := client.Get("test.key")
    if err != nil {
        log.Printf("Expected error for missing key: %v", err)
    }
    
    fmt.Println("โœ… ampy-config working correctly!")
}
EOF

๐Ÿ”— Integration Examples

With ampy-bus

Python integration:

import asyncio
from ampy_config.layering import build_effective_config
from ampy_config.bus.ampy_bus import AmpyBus

async def create_bus_from_config():
    # Build configuration
    cfg, _ = build_effective_config(
        schema_path="schema/ampy-config.schema.json",
        defaults_path="config/defaults.yaml",
        profile_yaml="examples/dev.yaml",
        overlays=[], service_overrides=[],
        env_allowlist_path="env_allowlist.txt",
        env_file=None,
        runtime_overrides_path="runtime/overrides.yaml",
    )
    
    # Get bus configuration
    nats_url = cfg["bus"]["nats_url"]
    stream_name = cfg["bus"]["stream_name"]
    topic_prefix = cfg["bus"]["topic_prefix"]
    
    # Create bus
    bus = AmpyBus(nats_url)
    await bus.connect()
    
    return bus, topic_prefix

Go integration:

import (
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
    "github.com/AmpyFin/ampy-bus/pkg/ampybus/natsbinding"
)

func createBusFromConfig() (*natsbinding.Bus, error) {
    // Create config client
    client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")
    
    // Get configuration values
    natsURL, err := client.Get("bus.nats_url")
    if err != nil {
        return nil, err
    }
    
    streamName, err := client.Get("bus.stream_name")
    if err != nil {
        return nil, err
    }
    
    topicPrefix, err := client.Get("bus.topic_prefix")
    if err != nil {
        return nil, err
    }
    
    // Create bus configuration
    config := natsbinding.Config{
        URLs:          []string{natsURL},
        StreamName:    streamName,
        Subjects:      []string{topicPrefix + ".>"},
        DurablePrefix: "ampy-trading",
    }
    
    return natsbinding.NewBus(config)
}

With ampy-proto

Go integration:

import (
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
    bars "github.com/AmpyFin/ampy-proto/v2/gen/go/ampy/bars/v1"
    common "github.com/AmpyFin/ampy-proto/v2/gen/go/ampy/common/v1"
)

func createBarFromConfig(client *ampyconfig.Client) (*bars.Bar, error) {
    // Get trading configuration
    symbol, err := client.Get("trading.symbol")
    if err != nil {
        return nil, err
    }
    
    mic, err := client.Get("trading.mic")
    if err != nil {
        return nil, err
    }
    
    // Create bar with config values
    bar := &bars.Bar{
        Security: &common.SecurityId{
            Symbol: symbol,
            Mic:    mic,
        },
        // ... other fields
    }
    
    return bar, nil
}

Dynamic Configuration Updates

Python:

async def setup_config_updates(bus, topic_prefix):
    async def on_config_apply(subject, data):
        print(f"Config applied: {data}")
        # Reload your application configuration
        reload_config()
    
    async def on_config_preview(subject, data):
        print(f"Config preview: {data}")
    
    # Subscribe to configuration events
    await bus.subscribe_json(f"{topic_prefix}.control.v1.config_apply", on_config_apply)
    await bus.subscribe_json(f"{topic_prefix}.control.v1.config_preview", on_config_preview)

def reload_config():
    # Reload your application configuration
    print("Reloading configuration...")

Go:

func setupConfigUpdates(client *ampyconfig.Client) error {
    // Subscribe to configuration changes
    err := client.Subscribe(func(event ampyconfig.ConfigEvent) {
        switch event.Type {
        case "config_preview":
            fmt.Printf("Config preview: %s\n", event.Data)
        case "config_apply":
            fmt.Printf("Config applied: %s\n", event.Data)
            // Reload configuration
            reloadConfig()
        }
    })
    
    return err
}

func reloadConfig() {
    // Reload your application configuration
    fmt.Println("Reloading configuration...")
}

โœ… Configuration Validation

Required Configuration Keys

Python validation:

def validate_config(cfg):
    """Validate that all required configuration keys are present"""
    required_keys = [
        "bus.nats_url",
        "bus.stream_name", 
        "bus.topic_prefix",
        "oms.risk.max_order_notional_usd",
        "trading.symbol",
        "trading.mic",
    ]
    
    missing_keys = []
    for key_path in required_keys:
        keys = key_path.split('.')
        current = cfg
        try:
            for key in keys:
                current = current[key]
        except (KeyError, TypeError):
            missing_keys.append(key_path)
    
    if missing_keys:
        raise ValueError(f"Missing required config keys: {missing_keys}")
    
    return True

# Usage
try:
    cfg, _ = build_effective_config(...)
    validate_config(cfg)
    print("โœ… Configuration validation passed")
except ValueError as e:
    print(f"โŒ Configuration validation failed: {e}")

Go validation:

func validateConfig(client *ampyconfig.Client) error {
    requiredKeys := []string{
        "bus.nats_url",
        "bus.stream_name",
        "bus.topic_prefix",
        "trading.symbol",
        "trading.mic",
    }
    
    for _, key := range requiredKeys {
        if _, err := client.Get(key); err != nil {
            return fmt.Errorf("missing required config key: %s", key)
        }
    }
    
    return nil
}

// Usage
if err := validateConfig(client); err != nil {
    log.Fatalf("Configuration validation failed: %v", err)
}

Schema Validation

Validate configuration files:

# Validate single file
python tools/validate.py examples/dev.yaml

# Validate multiple files
python tools/validate.py examples/*.yaml

# Validate with explicit schema
python tools/validate.py --schema schema/ampy-config.schema.json examples/dev.yaml

Programmatic validation:

from ampy_config.layering import build_effective_config

try:
    cfg, provenance = build_effective_config(
        schema_path="schema/ampy-config.schema.json",  # Enables schema validation
        defaults_path="config/defaults.yaml",
        profile_yaml="examples/dev.yaml",
        overlays=[],
        service_overrides=[],
        env_allowlist_path="env_allowlist.txt",
        env_file=None,
        runtime_overrides_path="runtime/overrides.yaml",
    )
    print("โœ… Schema validation passed")
except Exception as e:
    print(f"โŒ Schema validation failed: {e}")

Configuration File Examples

defaults.yaml:

# Default configuration values
bus:
  nats_url: "nats://localhost:4222"
  stream_name: "AMPY_TRADING"
  topic_prefix: "ampy.dev"
  durable_prefix: "ampy-trading"

trading:
  symbol: "AAPL"
  mic: "XNAS"
  risk_limit: 10000
  position_limit: 1000

signals:
  ma_short_period: 10
  ma_long_period: 20
  threshold: 0.5

logging:
  level: "info"
  format: "json"

development.yaml:

# Development overrides
bus:
  nats_url: "nats://localhost:4222"
  topic_prefix: "ampy.dev"

trading:
  symbol: "AAPL"
  risk_limit: 1000  # Lower limit for dev

logging:
  level: "debug"
  format: "text"

production.yaml:

# Production overrides
bus:
  nats_url: "nats://prod-nats:4222"
  topic_prefix: "ampy.prod"

trading:
  risk_limit: 100000
  position_limit: 10000

logging:
  level: "warn"
  format: "json"

runtime/overrides.yaml:

# Runtime dynamic overrides (can be updated via NATS)
trading:
  risk_limit: 5000  # Updated during runtime

signals:
  threshold: 0.7    # Updated during runtime

๐ŸŽฎ Control plane (NATS/JetStream)

Start a local NATS with JetStream:

docker run --rm -d --name nats -p 4222:4222 nats:2.10 -js
export NATS_URL="nats://127.0.0.1:4222"

Provision the stream and durable consumers (once). Using the nats CLI:

# Stream to cover all control-plane subjects
nats --server "$NATS_URL" stream add ampy-control \
  --subjects "ampy.*.control.v1.*" \
  --retention limits --max-age 24h --storage file \
  --max-msgs 10000 --max-bytes 100MB --discard old --defaults

# Agent durables (pull + explicit ack)
nats --server "$NATS_URL" consumer add ampy-control ampy-config-agent-ampy-dev-control-v1-config-preview \
  --filter "ampy.dev.control.v1.config_preview" --pull --deliver all --ack explicit --defaults
nats --server "$NATS_URL" consumer add ampy-control ampy-config-agent-ampy-dev-control-v1-config-apply \
  --filter "ampy.dev.control.v1.config_apply" --pull --deliver all --ack explicit --defaults
nats --server "$NATS_URL" consumer add ampy-control ampy-config-agent-ampy-dev-control-v1-secret-rotated \
  --filter "ampy.dev.control.v1.secret_rotated" --pull --deliver all --ack explicit --defaults

Verify setup:

nats --server "$NATS_URL" stream ls
nats --server "$NATS_URL" consumer ls ampy-control

โšก Note: The library can also auto-provision if permitted, but explicit creation is more predictable for local dev and CI.


๐Ÿ“š Layering model

Effective config = merge in this order (later overrides earlier):

graph TD
    A[๐Ÿ“„ Defaults<br/>config/defaults.yaml] --> B[๐ŸŒ Environment Profile<br/>examples/dev.yaml]
    B --> C[๐Ÿ“‹ Overlays<br/>--overlay path]
    C --> D[๐Ÿ”ง ENV Allowlist<br/>env_allowlist.txt]
    D --> E[โšก Runtime Overrides<br/>runtime/overrides.yaml]
    E --> F[โœ… Final Config]
Layer Description Example
1๏ธโƒฃ Defaults Checked-in base config config/defaults.yaml
2๏ธโƒฃ Environment profile Environment-specific settings examples/dev.yaml, examples/paper.yaml, examples/prod.yaml
3๏ธโƒฃ Overlays Region/cluster/service YAMLs --overlay path (repeatable)
4๏ธโƒฃ ENV allowlist Environment variable mapping env_allowlist.txt maps allowed env keys
5๏ธโƒฃ Runtime overrides Live configuration updates runtime/overrides.yaml (written by agent)

Each key tracks provenance: where it came from (defaults/profile/overlay/ENV/runtime).

๐Ÿ“ Units & types

Type Format Examples
โฑ๏ธ Durations String format 150ms, 2s, 5m, 1h
๐Ÿ“Š Sizes String format 128KiB, 1MiB
๐Ÿท๏ธ Domains Explicit prefixes oms.*, ingest.*, broker.*, ml.*, warehouse.*, fx.*, metrics, logging, tracing, security.*, feature_flags.*

๐Ÿ” Secrets (indirection, caching, rotation, redaction)

Use references, not literal values:

Backend Format Example
๐Ÿ” Vault secret://vault/<path>#<key> secret://vault/tiingo#token
โ˜๏ธ AWS SM aws-sm://<name>?versionStage=AWSCURRENT aws-sm://ALPACA_SECRET?versionStage=AWSCURRENT
๐ŸŒ GCP SM gcp-sm://projects/<project>/secrets/<name>/versions/latest gcp-sm://projects/demo/secrets/AMPY_API/versions/latest

Local development fallback file (.secrets.local.json):

{
  "secret://vault/tiingo#token": "TIINGO_LOCAL_DEV_TOKEN",
  "aws-sm://ALPACA_SECRET?versionStage=AWSCURRENT": "ALPACA_LOCAL_DEV_SECRET",
  "gcp-sm://projects/demo/secrets/AMPY_API/versions/latest": "AMPY_LOCAL_DEV_API"
}

๐Ÿ”’ Security: Secrets are always redacted in logs/metrics/traces; rotation is signaled via secret_rotated events.


๐Ÿ’ป CLI usage

All commands are available via python -m ampy_config.cli โ€ฆ (works without global entrypoints).

๐ŸŽจ Render effective config

python -m ampy_config.cli render \
  --profile dev \
  --resolve-secrets redacted \
  --provenance

Write it to a file:

python -m ampy_config.cli render \
  --profile dev \
  --resolve-secrets redacted \
  --output /tmp/effective.yaml

Resolve values (dev only; requires .secrets.local.json or configured backends):

AMPY_CONFIG_LOCAL_SECRETS=.secrets.local.json \
python -m ampy_config.cli render --profile dev --resolve-secrets values

โœ… Validate (schema + semantic checks)

python tools/validate.py examples/dev.yaml
# Or explicitly:
python tools/validate.py --schema schema/ampy-config.schema.json examples/*.yaml

๐Ÿ”ง Secrets utilities

# Resolve (redacted by default)
python -m ampy_config.cli secret get "aws-sm://ALPACA_SECRET?versionStage=AWSCURRENT"

# Print plain (development only)
python -m ampy_config.cli secret get --plain "secret://vault/tiingo#token"

# Invalidate cache entry
python -m ampy_config.cli secret rotate "gcp-sm://projects/demo/secrets/AMPY_API/versions/latest"

๐Ÿค– Run the agent

export NATS_URL="nats://127.0.0.1:4222"
export AMPY_CONFIG_SERVICE="ampy-config-agent"

python -m ampy_config.cli agent --profile dev

It subscribes to:

ampy.dev.control.v1.config_preview
ampy.dev.control.v1.config_apply
ampy.dev.control.v1.secret_rotated

โšก Ops: preview & apply a runtime override

Create an overlay:

cat >/tmp/overlay.yaml <<'YAML'
oms:
  risk:
    max_order_notional_usd: 77777
YAML

Preview (validate only):

python -m ampy_config.cli ops preview \
  --profile dev \
  --overlay-file /tmp/overlay.yaml \
  --expires-at "2025-12-31T23:59:59Z" \
  --reason "intraday risk tightening" \
  --dry-run

Apply (persist) and wait until it's effective in the resolved view:

python -m ampy_config.cli ops apply \
  --profile dev \
  --overlay-file /tmp/overlay.yaml \
  --wait-applied --timeout 20

Then verify:

python -m ampy_config.cli render \
  --profile dev \
  --runtime runtime/overrides.yaml \
  --resolve-secrets redacted \
  --provenance

๐Ÿ Use from a service (Python example)

# examples/service_skel.py
import asyncio, os
from ampy_config.layering import build_effective_config
from ampy_config.bus.ampy_bus import AmpyBus
from ampy_config.control.events import subjects

async def main():
    cfg, _ = build_effective_config(
        schema_path="schema/ampy-config.schema.json",
        defaults_path="config/defaults.yaml",
        profile_yaml="examples/dev.yaml",
        overlays=[],
        service_overrides=[],
        env_allowlist_path="env_allowlist.txt",
        env_file=None,
        runtime_overrides_path="runtime/overrides.yaml",
    )
    print("[service] max_order_notional_usd =", cfg["oms"]["risk"]["max_order_notional_usd"])

    bus = AmpyBus(os.environ.get("NATS_URL"))
    await bus.connect()
    subs = subjects(cfg["bus"]["topic_prefix"])

    async def on_apply(subject, data):
        # Re-build after apply; in real code, youโ€™d update state atomically & validate
        new_cfg, _ = build_effective_config(
            "schema/ampy-config.schema.json",
            "config/defaults.yaml",
            "examples/dev.yaml",
            [], [], "env_allowlist.txt", None, "runtime/overrides.yaml"
        )
        print("[service] updated max_order_notional_usd =", new_cfg["oms"]["risk"]["max_order_notional_usd"])

    await bus.subscribe_json(subs["apply"], on_apply)
    while True:
        await asyncio.sleep(1)

if __name__ == "__main__":
    os.environ.setdefault("AMPY_CONFIG_SERVICE", "ampy-service-demo")
    os.environ.setdefault("NATS_URL", "nats://127.0.0.1:4222")
    asyncio.run(main())

๐Ÿน Go Client Usage

๐Ÿ“š API Reference

Core Types:

// Client structure
type Client struct {
    // ... internal fields
}

// Configuration event
type ConfigEvent struct {
    Type string
    Data string
}

// Configuration methods
func New(natsURL, topicPrefix, configPath string) *Client
func (c *Client) Get(key string) (string, error)
func (c *Client) GetAll() (map[string]interface{}, error)
func (c *Client) Subscribe(handler func(ConfigEvent)) error
func (c *Client) Close() error

Key Methods:

// Create client
func New(natsURL, topicPrefix, configPath string) *Client

// Get configuration value
func (c *Client) Get(key string) (string, error)

// Get all configuration
func (c *Client) GetAll() (map[string]interface{}, error)

// Subscribe to config events
func (c *Client) Subscribe(handler func(ConfigEvent)) error

// Close client
func (c *Client) Close() error

๐Ÿš€ Basic Usage

Create and use client:

package main

import (
    "fmt"
    "log"
    
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
)

func main() {
    // Create ampy-config client
    client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")
    defer client.Close()
    
    // Get configuration values
    natsURL, err := client.Get("bus.nats_url")
    if err != nil {
        log.Fatal(err)
    }
    
    topicPrefix, err := client.Get("bus.topic_prefix")
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("NATS URL: %s\n", natsURL)
    fmt.Printf("Topic Prefix: %s\n", topicPrefix)
}

Subscribe to configuration changes:

func setupConfigUpdates(client *ampyconfig.Client) error {
    // Subscribe to configuration changes
    err := client.Subscribe(func(event ampyconfig.ConfigEvent) {
        switch event.Type {
        case "config_preview":
            fmt.Printf("Config preview: %s\n", event.Data)
        case "config_apply":
            fmt.Printf("Config applied: %s\n", event.Data)
            // Reload configuration
            reloadConfig()
        }
    })
    
    return err
}

func reloadConfig() {
    // Reload your application configuration
    fmt.Println("Reloading configuration...")
}

๐Ÿ› ๏ธ Command Line Tools

Start the agent:

./bin/ampyconfig-agent \
  -nats "$NATS_URL" \
  -topic ampy/dev \
  -runtime runtime/overrides.yaml \
  -service ampy-config-agent \
  -log info

Apply configuration changes:

cat >/tmp/overlay.yaml <<'YAML'
oms:
  risk:
    max_order_notional_usd: 123456
YAML

./bin/ampyconfig-ops \
  -nats "$NATS_URL" \
  -topic ampy/dev \
  -overlay-file /tmp/overlay.yaml \
  -wait-applied -timeout 20 \
  -runtime runtime/overrides.yaml

Available binaries:

  • ampyconfig-ops โ€” publish config_preview, config_apply, secret_rotated
  • ampyconfig-agent โ€” consume control events and persist runtime/overrides.yaml
  • ampyconfig-listener โ€” example service listener that reacts to changes

๐Ÿ“ Status: v0 thin client โ€” Python ampy-config remains the source of truth for schema validation and layering. This Go module focuses on control-plane parity and operational UX.

๐ŸŒ Go / C++ services

  • Parse the effective YAML (rendered by ops at boot or on a schedule)
  • Subscribe to the same control-plane subjects and re-load your resolved config (or just read runtime/overrides.yaml) when a config_apply is observed
  • Keep reloads transactional for safety-critical domains

๐Ÿ“Š Schema notes (metrics example)

The schema allows either OTLP (with endpoint) or Prometheus (with port):

"metrics": {
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "exporter": { "type": "string", "enum": ["otlp", "prom"] },
    "endpoint": { "type": "string" },
    "sampling_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
    "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
  },
  "required": ["exporter"]
}

Examples:

# OTLP
metrics:
  exporter: otlp
  endpoint: https://otel.dev.ampyfin.com:4317
  sampling_ratio: 0.25

# Prometheus
metrics:
  exporter: prom
  port: 9464

๐ŸŒ Environment variables

Variable Description Example
NATS_URL NATS server URL nats://127.0.0.1:4222
AMPY_CONFIG_SERVICE Logical service name (used to derive durable names) ampy-config-agent
AMPY_CONFIG_RUNTIME_OVERRIDES Path for persisted runtime overrides runtime/overrides.yaml
AMPY_CONFIG_LOCAL_SECRETS Path to local dev secrets JSON .secrets.local.json
AMPY_CONFIG_SECRET_TTL_MS Secrets cache TTL in milliseconds 120000
AMPY_CONFIG_JS_FALLBACK Force direct NATS subscription fallback 1 (skip JetStream)

Secret Backend Variables:

  • ๐Ÿ” Vault: VAULT_ADDR, VAULT_TOKEN (if using secret://)
  • โ˜๏ธ AWS: AWS_DEFAULT_REGION + credentials (if using aws-sm://)
  • ๐ŸŒ GCP: GOOGLE_APPLICATION_CREDENTIALS (if using gcp-sm://)

๐Ÿ”ง Troubleshooting

๐Ÿšจ Common Issues & Solutions

Issue Cause Solution
nats: no servers available for connection NATS server is not running Start NATS: docker run --rm -d --name nats -p 4222:4222 nats:2.10 -js
config key not found Configuration key doesn't exist Check your YAML files and ensure the key path is correct (e.g., bus.nats_url)
yaml: unmarshal errors Invalid YAML syntax Validate YAML syntax in your configuration files
permission denied File permission issues Check file permissions for configuration files and runtime directory
Configuration not updating Not subscribing to config events Ensure you're subscribing to config_apply events and handling updates
๐Ÿค– Agent only shows one subscription Blocked while initializing a secret backend Unset or configure that backend properly, or run with only local secrets in dev
โฐ No messages consumed / timeouts NATS_URL points to wrong port, JetStream disabled Check NATS_URL, enable JetStream, verify ampy-control stream & consumers exist
โŒ Apply says OK but value didn't change Agent didn't write runtime/overrides.yaml Verify file path via AMPY_CONFIG_RUNTIME_OVERRIDES and service reloads on config_apply
โš ๏ธ Schema validation passes but semantic check fails Semantic checks run after schema validation Fix the offending values called out in the error

๐Ÿ” Debug Mode

Python debug:

# Enable debug logging
import logging
logging.basicConfig(level=logging.DEBUG)

# Add debug logging to your config loading
def debug_config(cfg):
    if cfg.get("debug", {}).get("enabled") == True:
        print("๐Ÿ” DEBUG: Configuration loaded successfully")
        print(f"๐Ÿ” DEBUG: All config: {cfg}")

# Usage
cfg, _ = build_effective_config(...)
debug_config(cfg)

Go debug:

// Enable debug logging
client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")

// Add debug logging
func debugConfig(client *ampyconfig.Client) {
    if value, err := client.Get("debug.enabled"); err == nil && value == "true" {
        fmt.Println("๐Ÿ” DEBUG: Configuration loaded successfully")
        
        // Print all configuration
        if allConfig, err := client.GetAll(); err == nil {
            fmt.Printf("๐Ÿ” DEBUG: All config: %+v\n", allConfig)
        }
    }
}

๐Ÿงช Quick Health Check

Test NATS connection:

# Test NATS server
nats --server "nats://localhost:4222" server info

# Test JetStream
nats --server "nats://localhost:4222" stream ls

Test configuration loading:

# Python
python -c "
from ampy_config.layering import build_effective_config
try:
    cfg, _ = build_effective_config(
        'schema/ampy-config.schema.json',
        'config/defaults.yaml', 
        'examples/dev.yaml',
        [], [], 'env_allowlist.txt', None, 'runtime/overrides.yaml'
    )
    print('โœ… Configuration loading works!')
except Exception as e:
    print(f'โŒ Error: {e}')
"

# Go
go run - << 'EOF'
package main
import (
    "fmt"
    "github.com/AmpyFin/ampy-config/go/ampyconfig"
)
func main() {
    client := ampyconfig.New("nats://localhost:4222", "ampy.dev", "runtime/overrides.yaml")
    fmt.Println("โœ… ampy-config client created successfully!")
}
EOF

๐Ÿ›ก๏ธ Security notes

  • ๐Ÿ”’ Secrets are never logged; redaction is enforced throughout the library
  • ๐Ÿšจ Prefer fail-shut for safety-critical domains (OMS risk, broker creds) and fail-open for low-risk knobs (metric sampling)
  • ๐Ÿ” Ensure access to secret backends is locked down with least privilege

๐Ÿค Contributing

PRs welcome! Please include tests for new config keys, validation rules, and control-plane flows.

Before submitting:

pytest -q
python tools/validate.py examples/*.yaml

๐Ÿ“„ License

Apache-2.0 (proposed). See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ampy_config-1.1.5.tar.gz (47.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ampy_config-1.1.5-py3-none-any.whl (62.1 kB view details)

Uploaded Python 3

File details

Details for the file ampy_config-1.1.5.tar.gz.

File metadata

  • Download URL: ampy_config-1.1.5.tar.gz
  • Upload date:
  • Size: 47.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for ampy_config-1.1.5.tar.gz
Algorithm Hash digest
SHA256 6504ac787c347de8952f3513719d752541f692edf460e1ddfdc828d1ded74842
MD5 1649a0277a3b5c0904d7ebc8a1ebdc7f
BLAKE2b-256 8ade39e5c8bb6ec77a4efdd40a81f48af0187905cf7f163d62c8e993c546d071

See more details on using hashes here.

File details

Details for the file ampy_config-1.1.5-py3-none-any.whl.

File metadata

  • Download URL: ampy_config-1.1.5-py3-none-any.whl
  • Upload date:
  • Size: 62.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for ampy_config-1.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 524b634308018e277c3852c3c812a9fab75fda250659d65b19a82842613f7bfc
MD5 88007566b0eead5d28404ca3834319fc
BLAKE2b-256 c54811aeb62c22bef5c0ac0dd588f4406f83d1e1f9d81373d9a4566fda245d8f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page