Skip to content

Commit 931367d

Browse files
committed
feat(f1): allow programmatic overrides for env settings
Add functional options that let callers configure Prometheus and logging settings without relying on environment variables. New options: WithPrometheusPushGateway, WithPrometheusNamespace, WithPrometheusLabelID WithLogFilePath, WithLogLevel, WithLogFormat WithoutEnvSettings Precedence: programmatic options > env vars > defaults. WithLogger takes precedence over log level/format options. Construction order in New(): 1. Load settings from environment variables 2. Apply options (overrides or WithoutEnvSettings) 3. Build default output from final settings unless WithLogger was used Default behavior (no new options) is unchanged. Tests cover: env vars used by default, programmatic push gateway override, log level/format application, WithLogger precedence over log options, and WithoutEnvSettings ignoring env vars.
1 parent c99b6bf commit 931367d

File tree

4 files changed

+188
-10
lines changed

4 files changed

+188
-10
lines changed

.golangci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ linters:
8282
# Test files - relaxed rules for readability
8383
- linters: [dupword, lll, unparam, wrapcheck]
8484
path: _test\.go
85+
# t.Setenv is incompatible with t.Parallel (Go constraint)
86+
- linters: [paralleltest, tparallel]
87+
path: options_settings_test\.go
8588
- linters: [staticcheck]
8689
path: _test\.go
8790
text: ST1003

pkg/f1/f1.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,21 @@ type F1 struct {
3434
}
3535

3636
type f1Options struct {
37-
output *ui.Output
38-
staticMetrics map[string]string
37+
output *ui.Output
38+
staticMetrics map[string]string
39+
loggerExplicit bool
3940
}
4041

4142
// Option configures an F1 instance at construction.
4243
type Option func(*F1)
4344

4445
// WithLogger specifies the logger for internal and scenario logs.
45-
// This disables F1_LOG_LEVEL and F1_LOG_FORMAT.
46+
// When used, WithLogLevel, WithLogFormat, F1_LOG_LEVEL and F1_LOG_FORMAT
47+
// have no effect because the caller controls the logger directly.
4648
func WithLogger(logger *slog.Logger) Option {
4749
return func(f *F1) {
4850
f.options.output = ui.NewDefaultOutputWithLogger(logger)
51+
f.options.loggerExplicit = true
4952
}
5053
}
5154

@@ -57,21 +60,26 @@ func WithStaticMetrics(labels map[string]string) Option {
5760
}
5861

5962
// New instantiates a new F1 CLI. Pass options to configure logger, metrics, etc.
63+
//
64+
// Construction order:
65+
// 1. Load settings from environment variables
66+
// 2. Apply options (may override individual settings or clear them via WithoutEnvSettings)
67+
// 3. Build default output from final settings unless WithLogger was used
6068
func New(opts ...Option) *F1 {
61-
settings := envsettings.Get()
62-
6369
f := &F1{
6470
scenarios: scenarios.New(),
6571
profiling: &profiling{},
66-
settings: settings,
67-
options: &f1Options{
68-
output: ui.NewDefaultOutput(settings.Log.SlogLevel(), settings.Log.IsFormatJSON()),
69-
staticMetrics: nil,
70-
},
72+
settings: envsettings.Get(),
73+
options: &f1Options{},
7174
}
7275
for _, opt := range opts {
7376
opt(f)
7477
}
78+
79+
if !f.options.loggerExplicit {
80+
f.options.output = ui.NewDefaultOutput(f.settings.Log.SlogLevel(), f.settings.Log.IsFormatJSON())
81+
}
82+
7583
return f
7684
}
7785

pkg/f1/options_settings.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package f1
2+
3+
import "github.com/form3tech-oss/f1/v3/internal/envsettings"
4+
5+
// WithPrometheusPushGateway overrides the PROMETHEUS_PUSH_GATEWAY env var.
6+
func WithPrometheusPushGateway(url string) Option {
7+
return func(f *F1) {
8+
f.settings.Prometheus.PushGateway = url
9+
}
10+
}
11+
12+
// WithPrometheusNamespace overrides the PROMETHEUS_NAMESPACE env var.
13+
func WithPrometheusNamespace(ns string) Option {
14+
return func(f *F1) {
15+
f.settings.Prometheus.Namespace = ns
16+
}
17+
}
18+
19+
// WithPrometheusLabelID overrides the PROMETHEUS_LABEL_ID env var.
20+
func WithPrometheusLabelID(id string) Option {
21+
return func(f *F1) {
22+
f.settings.Prometheus.LabelID = id
23+
}
24+
}
25+
26+
// WithLogFilePath overrides the LOG_FILE_PATH env var.
27+
func WithLogFilePath(path string) Option {
28+
return func(f *F1) {
29+
f.settings.Log.FilePath = path
30+
}
31+
}
32+
33+
// WithLogLevel overrides the F1_LOG_LEVEL env var.
34+
// Accepts "debug", "info", "warn", "error" (case-insensitive).
35+
// Has no effect when WithLogger is also used.
36+
func WithLogLevel(level string) Option {
37+
return func(f *F1) {
38+
f.settings.Log.Level = level
39+
}
40+
}
41+
42+
// WithLogFormat overrides the F1_LOG_FORMAT env var.
43+
// Accepts "text" or "json" (case-insensitive).
44+
// Has no effect when WithLogger is also used.
45+
func WithLogFormat(format string) Option {
46+
return func(f *F1) {
47+
f.settings.Log.Format = format
48+
}
49+
}
50+
51+
// WithoutEnvSettings ignores all environment variables; settings start from
52+
// zero values (info level, text format, no prometheus). Must precede other
53+
// settings options in the option list so they are not overwritten.
54+
func WithoutEnvSettings() Option {
55+
return func(f *F1) {
56+
f.settings = envsettings.Settings{}
57+
}
58+
}

pkg/f1/options_settings_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package f1_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"net/http"
7+
"net/http/httptest"
8+
"sync/atomic"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/form3tech-oss/f1/v3/internal/log"
14+
"github.com/form3tech-oss/f1/v3/pkg/f1"
15+
"github.com/form3tech-oss/f1/v3/pkg/f1/f1testing"
16+
)
17+
18+
func newPushGatewayServer(t *testing.T) (*httptest.Server, *atomic.Int32) {
19+
t.Helper()
20+
21+
var count atomic.Int32
22+
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
23+
count.Add(1)
24+
}))
25+
t.Cleanup(ts.Close)
26+
27+
return ts, &count
28+
}
29+
30+
func newF1WithScenario(name string, opts ...f1.Option) *f1.F1 {
31+
inst := f1.New(opts...)
32+
inst.AddScenario(name, func(_ context.Context, _ *f1testing.T) f1testing.RunFn {
33+
return func(_ context.Context, _ *f1testing.T) {}
34+
})
35+
36+
return inst
37+
}
38+
39+
func runConstant(t *testing.T, inst *f1.F1, scenario string) {
40+
t.Helper()
41+
42+
err := inst.Run(context.Background(), []string{
43+
"run", "constant", scenario,
44+
"--rate", "1/1s",
45+
"--max-duration", "1s",
46+
"--max-iterations", "1",
47+
})
48+
require.NoError(t, err)
49+
}
50+
51+
func TestEnvVarsUsedByDefault(t *testing.T) {
52+
ts, count := newPushGatewayServer(t)
53+
t.Setenv("PROMETHEUS_PUSH_GATEWAY", ts.URL)
54+
55+
inst := newF1WithScenario("env_default")
56+
runConstant(t, inst, "env_default")
57+
58+
require.Positive(t, count.Load(),
59+
"PROMETHEUS_PUSH_GATEWAY env var should trigger metrics push")
60+
}
61+
62+
func TestWithPrometheusPushGatewayOverridesEnv(t *testing.T) {
63+
ts, count := newPushGatewayServer(t)
64+
t.Setenv("PROMETHEUS_PUSH_GATEWAY", "http://env-should-not-be-used.invalid")
65+
66+
inst := newF1WithScenario("override", f1.WithPrometheusPushGateway(ts.URL))
67+
runConstant(t, inst, "override")
68+
69+
require.Positive(t, count.Load(),
70+
"programmatic WithPrometheusPushGateway should override env var")
71+
}
72+
73+
func TestWithLogLevelAndFormat(t *testing.T) {
74+
t.Setenv("PROMETHEUS_PUSH_GATEWAY", "")
75+
76+
inst := newF1WithScenario("log_opts",
77+
f1.WithLogLevel("debug"),
78+
f1.WithLogFormat("json"),
79+
)
80+
runConstant(t, inst, "log_opts")
81+
}
82+
83+
func TestWithLoggerTakesPrecedenceOverLogOptions(t *testing.T) {
84+
var buf bytes.Buffer
85+
logger := log.NewTestLogger(&buf)
86+
87+
inst := newF1WithScenario("logger_precedence",
88+
f1.WithLogger(logger),
89+
f1.WithLogLevel("error"),
90+
f1.WithLogFormat("json"),
91+
)
92+
runConstant(t, inst, "logger_precedence")
93+
94+
output := buf.String()
95+
require.NotEmpty(t, output, "WithLogger's logger should capture output")
96+
require.NotContains(t, output, `"level"`,
97+
"explicit logger format (text) should be used, not JSON from WithLogFormat")
98+
}
99+
100+
func TestWithoutEnvSettingsIgnoresEnvVars(t *testing.T) {
101+
ts, count := newPushGatewayServer(t)
102+
t.Setenv("PROMETHEUS_PUSH_GATEWAY", ts.URL)
103+
104+
inst := newF1WithScenario("no_env", f1.WithoutEnvSettings())
105+
runConstant(t, inst, "no_env")
106+
107+
require.Equal(t, int32(0), count.Load(),
108+
"WithoutEnvSettings should prevent env var PROMETHEUS_PUSH_GATEWAY from being used")
109+
}

0 commit comments

Comments
 (0)