https://github.com/ampyfin/ampy-config
Typed config + secrets faรงade for AmpyFin: schema-validated, layered, redacted, and hot-reloaded via NATS/JetStream (Python + optional Go).
https://github.com/ampyfin/ampy-config
aws-secrets-manager configuration-management control-plane devops gcp-secret-manager hashicorp-vault hot-reload jsonschema observability pydantic runtime-configuration secrets-management trading-systems
Last synced: 14 days ago
JSON representation
Typed config + secrets faรงade for AmpyFin: schema-validated, layered, redacted, and hot-reloaded via NATS/JetStream (Python + optional Go).
- Host: GitHub
- URL: https://github.com/ampyfin/ampy-config
- Owner: AmpyFin
- License: apache-2.0
- Created: 2025-09-07T16:36:16.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2025-09-12T18:55:08.000Z (about 1 month ago)
- Last Synced: 2025-09-14T23:20:21.873Z (about 1 month ago)
- Topics: aws-secrets-manager, configuration-management, control-plane, devops, gcp-secret-manager, hashicorp-vault, hot-reload, jsonschema, observability, pydantic, runtime-configuration, secrets-management, trading-systems
- Language: Python
- Homepage: https://pypi.org/project/ampy-config/
- Size: 14.6 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# ๐ ampy-config
**Typed Configuration & Secrets Faรงade for AmpyFin**
[](https://www.python.org/downloads/)
[](LICENSE)
[](https://github.com/AmpyFin/ampy-config/actions)
[](https://pypi.org/project/ampy-config/)
[](https://github.com/psf/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](#-highlights-what-you-get) โข [๐ Quick Start](#-install-python--pypi) โข [๐ง Usage](#-cli-usage) โข [๐ค Contributing](#-contributing)
---
## ๐ Table of Contents
- [๐ฏ Why this exists](#-why-this-exists-the-problem)
- [โจ Highlights](#-highlights-what-you-get)
- [๐ Quick Start](#-quick-start)
- [๐ Install](#-install)
- [๐ฏ Basic Usage Examples](#-basic-usage-examples)
- [๐ Integration Examples](#-integration-examples)
- [โ Configuration Validation](#-configuration-validation)
- [๐ฎ Control Plane](#-control-plane-natsjetstream)
- [๐ Layering Model](#-layering-model)
- [๐ Secrets](#-secrets-indirection-caching-rotation-redaction)
- [๐ป CLI Usage](#-cli-usage)
- [๐ Python Integration](#-use-from-a-service-python-example)
- [๐น Go Client Usage](#-go-client-usage)
- [๐ Schema Examples](#-schema-notes-metrics-example)
- [๐ Environment Variables](#-environment-variables)
- [๐ง Troubleshooting](#-troubleshooting)
- [๐ก๏ธ Security](#-security-notes)
- [๐ค Contributing](#-contributing)---
## ๐ฏ 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:**
```bash
# 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:**
```bash
# 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
```bash
pip install ampy-config
```**Developer mode** (local repo):
```bash
pip install -e .
```### ๐น Go Client
**Library:**
```bash
go get github.com/AmpyFin/ampy-config/go/ampyconfig@v1.1.5
```**Binaries:**
```bash
cd go/ampyconfig
make # builds bin/ampyconfig-{ops,agent,listener}
```> ๐ฆ **Available on [pkg.go.dev](https://pkg.go.dev/github.com/AmpyFin/ampy-config/go/ampyconfig)**
### ๐ง 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
```python
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
```go
package mainimport (
"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:**
```bash
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:**
```bash
go run - << 'EOF'
package mainimport (
"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:**
```python
import asyncio
from ampy_config.layering import build_effective_config
from ampy_config.bus.ampy_bus import AmpyBusasync 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:**
```go
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:**
```go
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:**
```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:**
```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:**
```python
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:**
```go
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:**
```bash
# 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:**
```python
from ampy_config.layering import build_effective_configtry:
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:**
```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: 1000signals:
ma_short_period: 10
ma_long_period: 20
threshold: 0.5logging:
level: "info"
format: "json"
```**development.yaml:**
```yaml
# Development overrides
bus:
nats_url: "nats://localhost:4222"
topic_prefix: "ampy.dev"trading:
symbol: "AAPL"
risk_limit: 1000 # Lower limit for devlogging:
level: "debug"
format: "text"
```**production.yaml:**
```yaml
# Production overrides
bus:
nats_url: "nats://prod-nats:4222"
topic_prefix: "ampy.prod"trading:
risk_limit: 100000
position_limit: 10000logging:
level: "warn"
format: "json"
```**runtime/overrides.yaml:**
```yaml
# Runtime dynamic overrides (can be updated via NATS)
trading:
risk_limit: 5000 # Updated during runtimesignals:
threshold: 0.7 # Updated during runtime
```---
## ๐ฎ Control plane (NATS/JetStream)
**Start a local NATS with JetStream:**
```bash
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:
```bash
# 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:**
```bash
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):
```mermaid
graph TD
A[๐ Defaults
config/defaults.yaml] --> B[๐ Environment Profile
examples/dev.yaml]
B --> C[๐ Overlays
--overlay path]
C --> D[๐ง ENV Allowlist
env_allowlist.txt]
D --> E[โก Runtime Overrides
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/#` | `secret://vault/tiingo#token` |
| โ๏ธ **AWS SM** | `aws-sm://?versionStage=AWSCURRENT` | `aws-sm://ALPACA_SECRET?versionStage=AWSCURRENT` |
| ๐ **GCP SM** | `gcp-sm://projects//secrets//versions/latest` | `gcp-sm://projects/demo/secrets/AMPY_API/versions/latest` |**Local development fallback file** (`.secrets.local.json`):
```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
```bash
python -m ampy_config.cli render \
--profile dev \
--resolve-secrets redacted \
--provenance
```**Write it to a file:**
```bash
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):
```bash
AMPY_CONFIG_LOCAL_SECRETS=.secrets.local.json \
python -m ampy_config.cli render --profile dev --resolve-secrets values
```### โ Validate (schema + semantic checks)
```bash
python tools/validate.py examples/dev.yaml
# Or explicitly:
python tools/validate.py --schema schema/ampy-config.schema.json examples/*.yaml
```### ๐ง Secrets utilities
```bash
# 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
```bash
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:**
```bash
cat >/tmp/overlay.yaml <<'YAML'
oms:
risk:
max_order_notional_usd: 77777
YAML
```**Preview** (validate only):
```bash
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:
```bash
python -m ampy_config.cli ops apply \
--profile dev \
--overlay-file /tmp/overlay.yaml \
--wait-applied --timeout 20
```**Then verify:**
```bash
python -m ampy_config.cli render \
--profile dev \
--runtime runtime/overrides.yaml \
--resolve-secrets redacted \
--provenance
```---
## ๐ Use from a service (Python example)
```python
# 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 subjectsasync 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:**
```go
// 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:**
```go
// 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:**
```go
package mainimport (
"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:**
```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...")
}
```#### ๐ ๏ธ Command Line Tools
**Start the agent:**
```bash
./bin/ampyconfig-agent \
-nats "$NATS_URL" \
-topic ampy/dev \
-runtime runtime/overrides.yaml \
-service ampy-config-agent \
-log info
```**Apply configuration changes:**
```bash
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):
```json
"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:**
```yaml
# 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:**
```python
# 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:**
```go
// 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:**
```bash
# Test NATS server
nats --server "nats://localhost:4222" server info# Test JetStream
nats --server "nats://localhost:4222" stream ls
```**Test configuration loading:**
```bash
# 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:**
```bash
pytest -q
python tools/validate.py examples/*.yaml
```---
## ๐ License
Apache-2.0 (proposed). See `LICENSE` for details.