diff --git a/Procfile b/Procfile index fef0c2c..b74d072 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -cachewd **/*.go !**/*_test.go ready=http:8080/_readiness=200: cachewd --log-level=debug +cachewd **/*.go !**/*_test.go debounce=2s ready=http:8080/_readiness=200: CACHEW_LOG_LEVEL=debug cachewd diff --git a/cachew.hcl b/cachew.hcl index 107ecae..1f3d981 100644 --- a/cachew.hcl +++ b/cachew.hcl @@ -16,6 +16,8 @@ git-clone { mirror-root = "./state/git-mirrors" } +metrics {} + git { bundle-interval = "24h" snapshot-interval = "24h" diff --git a/cmd/cachewd/main.go b/cmd/cachewd/main.go index 789820d..d8cc11e 100644 --- a/cmd/cachewd/main.go +++ b/cmd/cachewd/main.go @@ -7,7 +7,6 @@ import ( "net" "net/http" "os" - "slices" "strings" "time" @@ -32,42 +31,45 @@ import ( type GlobalConfig struct { Bind string `hcl:"bind" default:"127.0.0.1:8080" help:"Bind address for the server."` URL string `hcl:"url" default:"http://127.0.0.1:8080/" help:"Base URL for cachewd."` - SchedulerConfig jobscheduler.Config `embed:"" hcl:"scheduler,block" prefix:"scheduler-"` - LoggingConfig logging.Config `embed:"" hcl:"log,block" prefix:"log-"` - MetricsConfig metrics.Config `embed:"" hcl:"metrics,block" prefix:"metrics-"` - GitCloneConfig gitclone.Config `embed:"" hcl:"git-clone,block" prefix:"git-clone-"` + SchedulerConfig jobscheduler.Config `hcl:"scheduler,block"` + LoggingConfig logging.Config `hcl:"log,block"` + MetricsConfig metrics.Config `hcl:"metrics,block"` + GitCloneConfig gitclone.Config `hcl:"git-clone,block"` } -var cli struct { //nolint:gochecknoglobals +type CLI struct { Schema bool `help:"Print the configuration file schema." xor:"command"` - Config kong.ConfigFlag `hcl:"-" help:"Configuration file path." placeholder:"PATH" required:"" default:"cachew.hcl"` - - // GlobalConfig accepts command-line, but can also be parsed from HCL. - GlobalConfig + Config *os.File `hcl:"-" help:"Configuration file path." required:"" default:"cachew.hcl"` } func main() { - kctx := kong.Parse(&cli, kong.DefaultEnvars("CACHEW"), kong.Configuration(config.KongLoader[GlobalConfig], "cachew.hcl")) + var cli CLI + kctx := kong.Parse(&cli, kong.DefaultEnvars("CACHEW")) - configReader, err := os.Open(string(cli.Config)) + defer cli.Config.Close() + ast, err := hcl.Parse(cli.Config) kctx.FatalIfErrorf(err) - defer configReader.Close() - ast, err := hcl.Parse(configReader) - kctx.FatalIfErrorf(err) + globalConfigHCL, providersConfigHCL := config.Split[GlobalConfig](ast) - _, providersConfig := config.Split[GlobalConfig](ast) + // Load global config. + var globalConfig GlobalConfig + globalSchema, err := hcl.Schema(&globalConfig) + kctx.FatalIfErrorf(err) + config.InjectEnvars(globalSchema, globalConfigHCL, "CACHEW", parseEnvars()) + err = hcl.UnmarshalAST(globalConfigHCL, &globalConfig, hcl.HydratedImplicitBlocks(true)) + kctx.FatalIfErrorf(err) ctx := context.Background() - logger, ctx := logging.Configure(ctx, cli.LoggingConfig) + logger, ctx := logging.Configure(ctx, globalConfig.LoggingConfig) // Start initialising - managerProvider := gitclone.NewManagerProvider(ctx, cli.GitCloneConfig) + managerProvider := gitclone.NewManagerProvider(ctx, globalConfig.GitCloneConfig) - scheduler := jobscheduler.New(ctx, cli.SchedulerConfig) + scheduler := jobscheduler.New(ctx, globalConfig.SchedulerConfig) - cr, sr := newRegistries(scheduler, managerProvider) + cr, sr := newRegistries(globalConfig.URL, scheduler, managerProvider) // Commands switch { //nolint:gocritic @@ -76,10 +78,10 @@ func main() { return } - mux, err := newMux(ctx, cr, sr, providersConfig) + mux, err := newMux(ctx, cr, sr, providersConfigHCL) kctx.FatalIfErrorf(err) - metricsClient, err := metrics.New(ctx, cli.MetricsConfig) + metricsClient, err := metrics.New(ctx, globalConfig.MetricsConfig) kctx.FatalIfErrorf(err, "failed to create metrics client") defer func() { if err := metricsClient.Close(); err != nil { @@ -91,14 +93,14 @@ func main() { kctx.FatalIfErrorf(err, "failed to start metrics server") } - logger.InfoContext(ctx, "Starting cachewd", slog.String("bind", cli.Bind)) + logger.InfoContext(ctx, "Starting cachewd", slog.String("bind", globalConfig.Bind)) - server := newServer(ctx, logger, mux) + server := newServer(ctx, mux, globalConfig.Bind, globalConfig.MetricsConfig) err = server.ListenAndServe() kctx.FatalIfErrorf(err) } -func newRegistries(scheduler jobscheduler.Scheduler, cloneManagerProvider gitclone.ManagerProvider) (*cache.Registry, *strategy.Registry) { +func newRegistries(cachewURL string, scheduler jobscheduler.Scheduler, cloneManagerProvider gitclone.ManagerProvider) (*cache.Registry, *strategy.Registry) { cr := cache.NewRegistry() cache.RegisterMemory(cr) cache.RegisterDisk(cr) @@ -108,7 +110,7 @@ func newRegistries(scheduler jobscheduler.Scheduler, cloneManagerProvider gitclo strategy.RegisterAPIV1(sr) strategy.RegisterArtifactory(sr) strategy.RegisterGitHubReleases(sr) - strategy.RegisterHermit(sr, cli.URL) + strategy.RegisterHermit(sr, cachewURL) strategy.RegisterHost(sr) git.Register(sr, scheduler, cloneManagerProvider) gomod.Register(sr, cloneManagerProvider) @@ -118,9 +120,6 @@ func newRegistries(scheduler jobscheduler.Scheduler, cloneManagerProvider gitclo func printSchema(kctx *kong.Context, cr *cache.Registry, sr *strategy.Registry) { schema := config.Schema[GlobalConfig](cr, sr) - slices.SortStableFunc(schema.Entries, func(a, b hcl.Entry) int { - return strings.Compare(a.EntryKey(), b.EntryKey()) - }) text, err := hcl.MarshalAST(schema) kctx.FatalIfErrorf(err) @@ -132,7 +131,7 @@ func printSchema(kctx *kong.Context, cr *cache.Registry, sr *strategy.Registry) } } -func newMux(ctx context.Context, cr *cache.Registry, sr *strategy.Registry, providersConfig *hcl.AST) (*http.ServeMux, error) { +func newMux(ctx context.Context, cr *cache.Registry, sr *strategy.Registry, providersConfigHCL *hcl.AST) (*http.ServeMux, error) { mux := http.NewServeMux() mux.HandleFunc("GET /_liveness", func(w http.ResponseWriter, _ *http.Request) { @@ -145,17 +144,18 @@ func newMux(ctx context.Context, cr *cache.Registry, sr *strategy.Registry, prov _, _ = w.Write([]byte("OK")) //nolint:errcheck }) - if err := config.Load(ctx, cr, sr, providersConfig, mux, parseEnvars()); err != nil { + if err := config.Load(ctx, cr, sr, providersConfigHCL, mux, parseEnvars()); err != nil { return nil, fmt.Errorf("load config: %w", err) } return mux, nil } -func newServer(ctx context.Context, logger *slog.Logger, mux *http.ServeMux) *http.Server { +func newServer(ctx context.Context, mux *http.ServeMux, bind string, metricsConfig metrics.Config) *http.Server { + logger := logging.FromContext(ctx) var handler http.Handler = mux - handler = otelhttp.NewMiddleware(cli.MetricsConfig.ServiceName, + handler = otelhttp.NewMiddleware(metricsConfig.ServiceName, otelhttp.WithMeterProvider(otel.GetMeterProvider()), otelhttp.WithTracerProvider(otel.GetTracerProvider()), )(handler) @@ -163,7 +163,7 @@ func newServer(ctx context.Context, logger *slog.Logger, mux *http.ServeMux) *ht handler = httputil.LoggingMiddleware(handler) return &http.Server{ - Addr: cli.Bind, + Addr: bind, Handler: handler, ReadTimeout: 30 * time.Minute, WriteTimeout: 30 * time.Minute, diff --git a/docker/Justfile b/docker/Justfile index 74a3751..5c73757 100644 --- a/docker/Justfile +++ b/docker/Justfile @@ -2,6 +2,7 @@ set positional-arguments := true set shell := ["bash", "-c"] # Configuration + ROOT := `git rev-parse --show-toplevel 2>/dev/null || echo "."` TAG := `git rev-parse --short HEAD 2>/dev/null || echo "dev"` VERSION := `git describe --tags --always --dirty 2>/dev/null || echo "dev"` @@ -41,7 +42,7 @@ build-multi: run log_level="info": @just build @echo "→ Starting cachew at http://localhost:8080 (log-level={{ log_level }})" - @docker run --rm -it -p 8080:8080 -v {{ ROOT }}/state:/app/state --name cachew cachew:local --log-level={{ log_level }} + @docker run --rm -it -p 8080:8080 -e CACHEW_LOG_LEVEL={{ log_level }} -v {{ ROOT }}/state:/app/state --name cachew cachew:local # Clean up Docker images clean: diff --git a/go.mod b/go.mod index e1a978a..d38e485 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/block/cachew go 1.25.5 require ( - github.com/alecthomas/hcl/v2 v2.5.0 + github.com/alecthomas/hcl/v2 v2.6.0 github.com/alecthomas/kong v1.13.0 github.com/goproxy/goproxy v0.25.0 github.com/lmittmann/tint v1.1.2 diff --git a/go.sum b/go.sum index 4fb62b8..c097480 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/errors v0.9.1 h1:JNXtU30rtMNARCkW41OTZ4yL6Lyocq20xIJgIw2raqI= github.com/alecthomas/errors v0.9.1/go.mod h1:l8mjMEHMGUdIWPMNtvDyRYPVS1fQFXHFXc/iVCCLGkI= -github.com/alecthomas/hcl/v2 v2.5.0 h1:0L0oGrZPHokiXaKtsEcLa3hBjfVrRLUUK3u5vXQSybg= -github.com/alecthomas/hcl/v2 v2.5.0/go.mod h1:4UUp66q8ony5j8tm2bANErujUpZ3GgHBLgaKxTUQlQI= +github.com/alecthomas/hcl/v2 v2.6.0 h1:5+3FFpVFP0PAKVmP66a32U5mVVa6C8VRx+w8HLqmGpo= +github.com/alecthomas/hcl/v2 v2.6.0/go.mod h1:4UUp66q8ony5j8tm2bANErujUpZ3GgHBLgaKxTUQlQI= github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= diff --git a/internal/config/config.go b/internal/config/config.go index 88462f2..ad85116 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,8 +4,12 @@ package config import ( "context" "log/slog" + "math/big" "net/http" "os" + "slices" + "strconv" + "strings" "github.com/alecthomas/errors" "github.com/alecthomas/hcl/v2" @@ -152,3 +156,120 @@ func expandVars(ast *hcl.AST, vars map[string]string) { return next() }) } + +// InjectEnvars walks the schema and for each attribute not present in the config, +// checks for a corresponding environment variable and injects it. +// +// Environment variable names are derived from the path to the attribute: +// prefix + block names + attr name, joined with "_", uppercased, hyphens replaced with "_". +// e.g. prefix="CACHEW", path=["scheduler", "concurrency"] -> "CACHEW_SCHEDULER_CONCURRENCY". +func InjectEnvars(schema *hcl.AST, config *hcl.AST, prefix string, vars map[string]string) { + container := &entryContainer{ast: config} + injectEntries(schema.Entries, container, []string{prefix}, vars) + _ = hcl.AddParentRefs(config) //nolint:errcheck +} + +// entryContainer abstracts over AST (top-level) and Block (nested) for inserting entries. +type entryContainer struct { + ast *hcl.AST + block *hcl.Block +} + +func (c *entryContainer) entries() hcl.Entries { + if c.block != nil { + return c.block.Body + } + return c.ast.Entries +} + +func (c *entryContainer) append(entry hcl.Entry) { + if c.block != nil { + c.block.Body = append(c.block.Body, entry) + } else { + c.ast.Entries = append(c.ast.Entries, entry) + } +} + +func (c *entryContainer) findBlock(name string) *entryContainer { + for _, e := range c.entries() { + if block, ok := e.(*hcl.Block); ok && block.Name == name { + return &entryContainer{ast: c.ast, block: block} + } + } + return nil +} + +func injectEntries(schemaEntries hcl.Entries, container *entryContainer, path []string, vars map[string]string) { + for _, entry := range schemaEntries { + switch entry := entry.(type) { + case *hcl.Attribute: + typ, ok := entry.Value.(*hcl.Type) + if !ok { + continue + } + envarName := pathToEnvar(append(slices.Clone(path), entry.Key)) + val, ok := vars[envarName] + if !ok { + continue + } + if hasAttr(container.entries(), entry.Key) { + continue + } + hclVal, err := parseValue(val, typ.Type) + if err != nil { + continue + } + container.append(&hcl.Attribute{Key: entry.Key, Value: hclVal}) + + case *hcl.Block: + child := container.findBlock(entry.Name) + if child == nil { + // Create a temporary container; only add the block to the + // config if at least one envar populated it. + tmp := &entryContainer{ast: container.ast, block: &hcl.Block{Name: entry.Name}} + injectEntries(entry.Body, tmp, append(path, entry.Name), vars) + if len(tmp.block.Body) > 0 { + container.append(tmp.block) + } + } else { + injectEntries(entry.Body, child, append(path, entry.Name), vars) + } + } + } +} + +func pathToEnvar(path []string) string { + s := strings.Join(path, "_") + s = strings.ReplaceAll(s, "-", "_") + return strings.ToUpper(s) +} + +func hasAttr(entries hcl.Entries, key string) bool { + for _, e := range entries { + if attr, ok := e.(*hcl.Attribute); ok && attr.Key == key { + return true + } + } + return false +} + +func parseValue(raw string, typ string) (hcl.Value, error) { + switch typ { + case "string": + return &hcl.String{Str: raw}, nil + case "number": + f, _, err := big.ParseFloat(raw, 10, 256, big.ToNearestEven) + if err != nil { + return nil, errors.Wrap(err, raw) + } + return &hcl.Number{Float: f}, nil + case "boolean": + b, err := strconv.ParseBool(raw) + if err != nil { + return nil, errors.Wrap(err, raw) + } + return &hcl.Bool{Bool: b}, nil + default: + return nil, errors.Errorf("unsupported type %q", typ) + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..ef0ad9d --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,135 @@ +package config //nolint:testpackage + +import ( + "strings" + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/hcl/v2" +) + +func TestInjectEnvars(t *testing.T) { + type Scheduler struct { + Concurrency int `hcl:"concurrency"` + } + type GitClone struct { + Depth int `hcl:"depth"` + Dir string `hcl:"dir"` + } + type Config struct { + Bind string `hcl:"bind"` + Scheduler Scheduler `hcl:"scheduler,block"` + GitClone GitClone `hcl:"git-clone,block"` + } + + schema, err := hcl.Schema(new(Config)) + assert.NoError(t, err) + + tests := []struct { + name string + config string + vars map[string]string + expected string + }{ + { + name: "InjectTopLevelAttr", + config: ``, + vars: map[string]string{"CACHEW_BIND": "0.0.0.0:9090"}, + expected: ` +bind = "0.0.0.0:9090" +`, + }, + { + name: "InjectNestedAttr", + config: `bind = "127.0.0.1:8080"`, + vars: map[string]string{"CACHEW_SCHEDULER_CONCURRENCY": "10"}, + expected: ` +bind = "127.0.0.1:8080" + +scheduler { + concurrency = 10 +} +`, + }, + { + name: "ExistingAttrNotOverwritten", + config: ` +bind = "127.0.0.1:8080" + +scheduler { + concurrency = 4 +} +`, + vars: map[string]string{"CACHEW_SCHEDULER_CONCURRENCY": "10"}, + expected: ` +bind = "127.0.0.1:8080" + +scheduler { + concurrency = 4 +} +`, + }, + { + name: "InjectIntoExistingBlock", + config: ` +git-clone { + depth = 1 +} +`, + vars: map[string]string{"CACHEW_GIT_CLONE_DIR": "/tmp/clones"}, + expected: ` +git-clone { + depth = 1 + dir = "/tmp/clones" +} +`, + }, + { + name: "NoMatchingEnvar", + config: `bind = "127.0.0.1:8080"`, + vars: map[string]string{"UNRELATED_VAR": "foo"}, + expected: ` +bind = "127.0.0.1:8080" +`, + }, + { + name: "EmptyBlockNotCreated", + config: ``, + vars: map[string]string{}, + expected: ``, + }, + { + name: "MultipleInjections", + config: ``, + vars: map[string]string{ + "CACHEW_BIND": "0.0.0.0:9090", + "CACHEW_SCHEDULER_CONCURRENCY": "8", + "CACHEW_GIT_CLONE_DEPTH": "3", + }, + expected: ` +bind = "0.0.0.0:9090" + +scheduler { + concurrency = 8 +} + +git-clone { + depth = 3 +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := hcl.Parse(strings.NewReader(tt.config)) + assert.NoError(t, err) + + InjectEnvars(schema, config, "CACHEW", tt.vars) + + got, err := hcl.MarshalAST(config) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(string(got))) + }) + } +} diff --git a/internal/config/kong.go b/internal/config/kong.go deleted file mode 100644 index e529b39..0000000 --- a/internal/config/kong.go +++ /dev/null @@ -1,119 +0,0 @@ -package config - -import ( - "fmt" - "io" - "strings" - - "github.com/alecthomas/hcl/v2" - "github.com/alecthomas/kong" -) - -func KongLoader[GlobalConfig any](r io.Reader) (kong.Resolver, error) { - ast, err := hcl.Parse(r) - if err != nil { - return nil, fmt.Errorf("failed to parse HCL: %w", err) - } - return &kongResolver{flattenHCL(ast)}, nil -} - -type kongResolver struct { - values map[string]any -} - -var _ kong.Resolver = (*kongResolver)(nil) - -func (k *kongResolver) Resolve(_ *kong.Context, _ *kong.Path, flag *kong.Flag) (any, error) { - name := strings.ReplaceAll(flag.Name, "-", "_") - if v, ok := k.values[name]; ok { - return v, nil - } - if v, ok := k.values[flag.Name]; ok { - return v, nil - } - return nil, nil //nolint:nilnil -} - -func (k *kongResolver) Validate(_ *kong.Application) error { return nil } - -// Convert HCL AST to a flattened map of key to value. Each hierarchy in the HCL joined by "-". -// -// eg. -// -// block { -// value = "foo" -// } -// -// block-with-label label { -// value = "foo" -// } -// -// Would flatten to: -// -// block-value = "foo" -// block-with-label-label = "foo" -func flattenHCL(node hcl.Node) map[string]any { - out := map[string]any{} - flattenNode(out, "", node) - return out -} - -func flattenNode(out map[string]any, prefix string, node hcl.Node) { - switch node := node.(type) { - case *hcl.AST: - for _, entry := range node.Entries { - flattenNode(out, prefix, entry) - } - - case *hcl.Block: - parts := make([]string, 0, 1+len(node.Labels)) - parts = append(parts, node.Name) - parts = append(parts, node.Labels...) - key := strings.Join(parts, "-") - if prefix != "" { - key = prefix + "-" + key - } - for _, entry := range node.Body { - flattenNode(out, key, entry) - } - - case *hcl.Attribute: - key := node.Key - if prefix != "" { - key = prefix + "-" + key - } - out[key] = hclValue(node.Value) - } -} - -func hclValue(v hcl.Value) any { - switch v := v.(type) { - case *hcl.String: - return v.Str - case *hcl.Number: - if v.Float.IsInt() { - i, _ := v.Float.Int64() - return i - } - f, _ := v.Float.Float64() - return f - case *hcl.Bool: - return v.Bool - case *hcl.List: - out := make([]any, len(v.List)) - for i, item := range v.List { - out[i] = hclValue(item) - } - return out - case *hcl.Map: - out := map[string]any{} - for _, entry := range v.Entries { - out[fmt.Sprintf("%v", hclValue(entry.Key))] = hclValue(entry.Value) - } - return out - case *hcl.Heredoc: - return v.GetHeredoc() - default: - return nil - } -} diff --git a/internal/config/kong_test.go b/internal/config/kong_test.go deleted file mode 100644 index 239ebd4..0000000 --- a/internal/config/kong_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package config //nolint:testpackage - -import ( - "strings" - "testing" - - "github.com/alecthomas/assert/v2" - "github.com/alecthomas/hcl/v2" -) - -func TestFlattenHCL(t *testing.T) { - tests := []struct { - name string - input string - expected map[string]any - }{ - { - name: "SimpleAttribute", - input: `value = "foo"`, - expected: map[string]any{ - "value": "foo", - }, - }, - { - name: "Block", - input: `block { - value = "foo" - }`, - expected: map[string]any{ - "block-value": "foo", - }, - }, - { - name: "BlockWithLabel", - input: `block-with-label label { - value = "foo" - }`, - expected: map[string]any{ - "block-with-label-label-value": "foo", - }, - }, - { - name: "NestedBlocks", - input: `outer { - inner { - value = "foo" - } - }`, - expected: map[string]any{ - "outer-inner-value": "foo", - }, - }, - { - name: "NumberInt", - input: `count = 42`, - expected: map[string]any{ - "count": int64(42), - }, - }, - { - name: "NumberFloat", - input: `ratio = 3.14`, - expected: map[string]any{ - "ratio": 3.14, - }, - }, - { - name: "Bool", - input: `enabled = true`, - expected: map[string]any{ - "enabled": true, - }, - }, - { - name: "List", - input: `tags = ["a", "b", "c"]`, - expected: map[string]any{ - "tags": []any{"a", "b", "c"}, - }, - }, - { - name: "Map", - input: `labels = {x: 1, y: 2}`, - expected: map[string]any{ - "labels": map[string]any{"x": int64(1), "y": int64(2)}, - }, - }, - { - name: "MultipleEntries", - input: ` - name = "test" - block { - port = 8080 - } - `, - expected: map[string]any{ - "name": "test", - "block-port": int64(8080), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ast, err := hcl.Parse(strings.NewReader(tt.input)) - assert.NoError(t, err) - actual := flattenHCL(ast) - assert.Equal(t, tt.expected, actual) - }) - } -} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 5ccc94b..37edb95 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -23,13 +23,13 @@ import ( // Config holds metrics configuration. type Config struct { - ServiceName string `help:"Service name for metrics." default:"cachew"` - Port int `help:"Port for Prometheus metrics server." default:"9102"` - EnablePrometheus bool `help:"Enable Prometheus exporter." default:"true"` - EnableOTLP bool `help:"Enable OTLP exporter." default:"false"` - OTLPEndpoint string `help:"OTLP endpoint URL." default:"http://localhost:4318"` - OTLPInsecure bool `help:"Use insecure connection for OTLP." default:"false"` - OTLPExportInterval int `help:"OTLP export interval in seconds." default:"60"` + ServiceName string `hcl:"service-name" help:"Service name for metrics." default:"cachew"` + Port int `hcl:"port" help:"Port for Prometheus metrics server." default:"9102"` + EnablePrometheus bool `hcl:"enable-prometheus" help:"Enable Prometheus exporter." default:"true"` + EnableOTLP bool `hcl:"enable-otlp" help:"Enable OTLP exporter." default:"false"` + OTLPEndpoint string `hcl:"otlp-endpoint" help:"OTLP endpoint URL." default:"http://localhost:4318"` + OTLPInsecure bool `hcl:"otlp-insecure" help:"Use insecure connection for OTLP." default:"false"` + OTLPExportInterval int `hcl:"otlp-export-interval" help:"OTLP export interval in seconds." default:"60"` } // Client provides OpenTelemetry metrics with configurable exporters.