Skip to content

Commit 572ca42

Browse files
committed
feat: implement configuration and structured logging
1 parent 9ae6ec6 commit 572ca42

2 files changed

Lines changed: 145 additions & 0 deletions

File tree

internal/config/config.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Package config handles configuration from environment variables.
2+
package config
3+
4+
import (
5+
"fmt"
6+
"os"
7+
"strconv"
8+
"strings"
9+
"time"
10+
)
11+
12+
// ReadOnlyMode defines how write requests are handled.
13+
type ReadOnlyMode string
14+
15+
const (
16+
ReadOnlyOff ReadOnlyMode = "false" // Full read/write passthrough
17+
ReadOnlyOn ReadOnlyMode = "true" // Silently ignore writes, return success
18+
ReadOnlyDeny ReadOnlyMode = "deny" // Reject writes with exception
19+
)
20+
21+
// Config holds the proxy configuration.
22+
type Config struct {
23+
Listen string
24+
Upstream string
25+
DefaultSlaveID byte
26+
CacheTTL time.Duration
27+
CacheServeStale bool
28+
ReadOnly ReadOnlyMode
29+
Timeout time.Duration
30+
ShutdownTimeout time.Duration
31+
LogLevel string
32+
}
33+
34+
// Load reads configuration from environment variables.
35+
func Load() (*Config, error) {
36+
cfg := &Config{
37+
Listen: getEnv("MODBUS_LISTEN", ":5502"),
38+
Upstream: os.Getenv("MODBUS_UPSTREAM"),
39+
DefaultSlaveID: 1,
40+
CacheTTL: 10 * time.Second,
41+
CacheServeStale: false,
42+
ReadOnly: ReadOnlyOn,
43+
Timeout: 10 * time.Second,
44+
ShutdownTimeout: 30 * time.Second,
45+
LogLevel: getEnv("LOG_LEVEL", "INFO"),
46+
}
47+
48+
if cfg.Upstream == "" {
49+
return nil, fmt.Errorf("MODBUS_UPSTREAM is required")
50+
}
51+
52+
// Parse slave ID
53+
if s := os.Getenv("MODBUS_SLAVE_ID"); s != "" {
54+
id, err := strconv.ParseUint(s, 10, 8)
55+
if err != nil {
56+
return nil, fmt.Errorf("invalid MODBUS_SLAVE_ID: %w", err)
57+
}
58+
cfg.DefaultSlaveID = byte(id)
59+
}
60+
61+
// Parse cache TTL
62+
if s := os.Getenv("MODBUS_CACHE_TTL"); s != "" {
63+
d, err := time.ParseDuration(s)
64+
if err != nil {
65+
return nil, fmt.Errorf("invalid MODBUS_CACHE_TTL: %w", err)
66+
}
67+
cfg.CacheTTL = d
68+
}
69+
70+
// Parse serve stale
71+
if s := os.Getenv("MODBUS_CACHE_SERVE_STALE"); s != "" {
72+
cfg.CacheServeStale = strings.ToLower(s) == "true"
73+
}
74+
75+
// Parse readonly mode
76+
if s := os.Getenv("MODBUS_READONLY"); s != "" {
77+
switch strings.ToLower(s) {
78+
case "false":
79+
cfg.ReadOnly = ReadOnlyOff
80+
case "true":
81+
cfg.ReadOnly = ReadOnlyOn
82+
case "deny":
83+
cfg.ReadOnly = ReadOnlyDeny
84+
default:
85+
return nil, fmt.Errorf("invalid MODBUS_READONLY: %s (must be false, true, or deny)", s)
86+
}
87+
}
88+
89+
// Parse timeout
90+
if s := os.Getenv("MODBUS_TIMEOUT"); s != "" {
91+
d, err := time.ParseDuration(s)
92+
if err != nil {
93+
return nil, fmt.Errorf("invalid MODBUS_TIMEOUT: %w", err)
94+
}
95+
cfg.Timeout = d
96+
}
97+
98+
// Parse shutdown timeout
99+
if s := os.Getenv("MODBUS_SHUTDOWN_TIMEOUT"); s != "" {
100+
d, err := time.ParseDuration(s)
101+
if err != nil {
102+
return nil, fmt.Errorf("invalid MODBUS_SHUTDOWN_TIMEOUT: %w", err)
103+
}
104+
cfg.ShutdownTimeout = d
105+
}
106+
107+
return cfg, nil
108+
}
109+
110+
func getEnv(key, defaultValue string) string {
111+
if v := os.Getenv(key); v != "" {
112+
return v
113+
}
114+
return defaultValue
115+
}

internal/logging/logging.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Package logging provides structured logging.
2+
package logging
3+
4+
import (
5+
"log/slog"
6+
"os"
7+
"strings"
8+
)
9+
10+
// New creates a new logger with the specified level.
11+
func New(level string) *slog.Logger {
12+
var lvl slog.Level
13+
switch strings.ToUpper(level) {
14+
case "DEBUG":
15+
lvl = slog.LevelDebug
16+
case "WARN", "WARNING":
17+
lvl = slog.LevelWarn
18+
case "ERROR":
19+
lvl = slog.LevelError
20+
default:
21+
lvl = slog.LevelInfo
22+
}
23+
24+
opts := &slog.HandlerOptions{
25+
Level: lvl,
26+
}
27+
28+
handler := slog.NewTextHandler(os.Stdout, opts)
29+
return slog.New(handler)
30+
}

0 commit comments

Comments
 (0)