Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
stdlog "log"
"os"
Expand Down Expand Up @@ -105,6 +106,28 @@ func buildCLI() *cli.App {
return nil
},
},
{
Name: "config",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrate this with the validate-dynamic-config command above (probably move that one into here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll pull in render-config as well

Usage: "Configuration related commands",
Subcommands: []*cli.Command{
{
Name: "dynamic",
Usage: "Show all available dynamic configuration keys",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
Aliases: []string{"j"},
Usage: "Output as JSON",
},
},
Action: func(c *cli.Context) error {
jsonOutput := c.Bool("json")
return showDynamicConfig(jsonOutput)
},
},
},
},
{
Name: "render-config",
Usage: "Render server config template",
Expand Down Expand Up @@ -258,3 +281,96 @@ func buildCLI() *cli.App {
}
return app
}

func showDynamicConfig(jsonOutput bool) error {
// Get all settings from the registry
settings := dynamicconfig.ListAllSettings()

if jsonOutput {
return outputJSON(settings)
}
return outputTable(settings)
}

func outputTable(settings []dynamicconfig.GenericSetting) error {
// Find max widths for table columns across all settings
maxKeyLen := len("KEY")
maxTypeLen := len("TYPE")
maxScopeLen := len("SCOPE")

for _, setting := range settings {
doc := setting.Documentation()
if len(doc.Key) > maxKeyLen {
maxKeyLen = len(doc.Key)
}
if len(doc.Type) > maxTypeLen {
maxTypeLen = len(doc.Type)
}
if len(doc.Precedence) > maxScopeLen {
maxScopeLen = len(doc.Precedence)
}
}

// Print table header
fmt.Printf("%-*s %-*s %-*s %s\n",
maxKeyLen, "KEY",
maxTypeLen, "TYPE",
maxScopeLen, "SCOPE",
"DEFAULT")
fmt.Printf("%s %s %s %s\n",
strings.Repeat("─", maxKeyLen),
strings.Repeat("─", maxTypeLen),
strings.Repeat("─", maxScopeLen),
strings.Repeat("─", 20))

// Print table rows
for _, setting := range settings {
doc := setting.Documentation()
defaultStr := fmt.Sprintf("%v", doc.DefaultValue)
if len(defaultStr) > 50 {
defaultStr = defaultStr[:47] + "..."
}

fmt.Printf("%-*s %-*s %-*s %s\n",
maxKeyLen, doc.Key,
maxTypeLen, doc.Type,
maxScopeLen, doc.Precedence,
defaultStr)
}

fmt.Println()
return nil
}

func outputJSON(settings []dynamicconfig.GenericSetting) error {
type jsonSetting struct {
Key string `json:"key"`
Type string `json:"type"`
Precedence string `json:"precedence"`
Description string `json:"description"`
DefaultValue any `json:"default,omitempty"`
}

var jsonSettings []jsonSetting
for _, setting := range settings {
doc := setting.Documentation()

// Try to encode default value, use string representation if it fails
defaultValue := doc.DefaultValue
if _, err := json.Marshal(defaultValue); err != nil {
defaultValue = fmt.Sprintf("%v", defaultValue)
}

jsonSettings = append(jsonSettings, jsonSetting{
Key: doc.Key,
Type: doc.Type,
Precedence: doc.Precedence,
Description: doc.Description,
DefaultValue: defaultValue,
})
}

encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(jsonSettings)
}
19 changes: 19 additions & 0 deletions cmd/tools/gendynamicconfig/dynamic_config.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,32 @@ func (s {{$P.Name}}TypedSetting[T]) Validate(v any) error {
_, err := s.convert(v)
return err
}
func (s {{$P.Name}}TypedSetting[T]) Documentation() SettingDoc {
return SettingDoc{
Key: s.key.String(),
Type: getTypeName(s.def),
Precedence: "{{$P.Name}}",
Description: s.description,
DefaultValue: formatDefaultValue(s.def),
}
}

func (s {{$P.Name}}TypedConstrainedDefaultSetting[T]) Key() Key { return s.key }
func (s {{$P.Name}}TypedConstrainedDefaultSetting[T]) Precedence() Precedence { return Precedence{{$P.Name}} }
func (s {{$P.Name}}TypedConstrainedDefaultSetting[T]) Validate(v any) error {
_, err := s.convert(v)
return err
}
func (s {{$P.Name}}TypedConstrainedDefaultSetting[T]) Documentation() SettingDoc {
var zero T
return SettingDoc{
Key: s.key.String(),
Type: getTypeName(zero),
Precedence: "{{$P.Name}}",
Description: s.description,
DefaultValue: formatConstrainedDefaults(s.cdef),
}
}

func (s {{$P.Name}}TypedSetting[T]) WithDefault(v T) {{$P.Name}}TypedSetting[T] {
newS := s
Expand Down
32 changes: 20 additions & 12 deletions cmd/tools/gendynamicconfig/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type (
settingType struct {
Name string
GoType string
TypeName string // lowercase type name for documentation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the value in having an extra value here over just using the Go type via reflection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot why I added it, I'll see if i can remove it

IsGeneric bool
}
settingPrecedence struct {
Expand All @@ -31,32 +32,39 @@ var (
data = dynamicConfigData{
Types: []settingType{
{
Name: "Bool",
GoType: "bool",
Name: "Bool",
GoType: "bool",
TypeName: "bool",
},
{
Name: "Int",
GoType: "int",
Name: "Int",
GoType: "int",
TypeName: "int",
},
{
Name: "Float",
GoType: "float64",
Name: "Float",
GoType: "float64",
TypeName: "float",
},
{
Name: "String",
GoType: "string",
Name: "String",
GoType: "string",
TypeName: "string",
},
{
Name: "Duration",
GoType: "time.Duration",
Name: "Duration",
GoType: "time.Duration",
TypeName: "duration",
},
{
Name: "Map",
GoType: "map[string]any",
Name: "Map",
GoType: "map[string]any",
TypeName: "map",
},
{
Name: "Typed",
GoType: "<generic>",
TypeName: "typed",
IsGeneric: true, // this one is treated differently
},
},
Expand Down
13 changes: 13 additions & 0 deletions common/dynamicconfig/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ func queryRegistry(k Key) GenericSetting {
return globalRegistry.settings[k.Lower()]
}

// ListAllSettings returns all registered dynamic config settings.
// This is intended for documentation generation and introspection.
func ListAllSettings() []GenericSetting {
if !globalRegistry.queried.Load() {
globalRegistry.queried.Store(true)
}
settings := make([]GenericSetting, 0, len(globalRegistry.settings))
for _, s := range globalRegistry.settings {
settings = append(settings, s)
}
return settings
}

// For testing only; do not call from regular code!
func ResetRegistryForTest() {
globalRegistry.settings = nil
Expand Down
103 changes: 103 additions & 0 deletions common/dynamicconfig/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

package dynamicconfig

import (
"reflect"
"strings"
"time"
)

type (
// Precedence is an enum for the search order precedence of a dynamic config setting.
// E.g., use the global value, check namespace then global, check task queue then
Expand All @@ -27,13 +33,23 @@ type (
description string // documentation
}

// SettingDoc contains documentation metadata for a dynamic config setting.
SettingDoc struct {
Key string // setting key name
Type string // setting value type (e.g., "bool", "int", "duration", "string", "float", "map", "typed")
Precedence string // setting precedence (e.g., "Global", "Namespace", "TaskQueue")
Description string // human-readable description
DefaultValue any // default value (simple value or []TypedConstrainedValue for constrained defaults)
}

// GenericSetting is an interface that all instances of Setting implement (by generated
// code in setting_gen.go). It can be used to refer to settings of any type and deal with
// them generically..
GenericSetting interface {
Key() Key
Precedence() Precedence
Validate(v any) error
Documentation() SettingDoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already getters for Key and Precedence, let's just add new individual getters for description and default value instead of a new struct


// for internal use:
dispatchUpdate(*Collection, any, []ConstrainedValue)
Expand All @@ -48,3 +64,90 @@ type (
DynamicConfigParseHook(S) (T, error)
}
)

// formatDefaultValue formats a default value for display, converting durations to strings
func formatDefaultValue(v any) any {
if d, ok := v.(time.Duration); ok {
return d.String()
}
return v
}

// formatConstrainedDefaults formats constrained default values, converting durations to strings
func formatConstrainedDefaults[T any](cdef []TypedConstrainedValue[T]) any {
// Create a copy with formatted values
result := make([]TypedConstrainedValue[any], len(cdef))
for i, cv := range cdef {
result[i] = TypedConstrainedValue[any]{
Constraints: cv.Constraints,
Value: formatDefaultValue(cv.Value),
}
}
return result
}

// getTypeName returns a human-readable type name for documentation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is all this complexity worth it instead of just fmt.Sprintf("%T", v)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can remove it.

func getTypeName(v any) string {
t := reflect.TypeOf(v)
if t == nil {
return "any"
}

// Handle common types
switch t.Kind() {
case reflect.Bool:
return "bool"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// Special case for time.Duration
if t.String() == "time.Duration" {
return "duration"
}
return "int"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "int"
case reflect.Float32, reflect.Float64:
return "float"
case reflect.String:
return "string"
case reflect.Map:
keyType := getTypeNameFromType(t.Key())
valueType := getTypeNameFromType(t.Elem())
return "map[" + keyType + "]" + valueType
case reflect.Slice:
elemType := getTypeNameFromType(t.Elem())
return "[]" + elemType
case reflect.Struct:
// Check for time.Duration and other known structs
typeName := t.String()
if strings.Contains(typeName, ".") {
// Remove package prefix for cleaner names
parts := strings.Split(typeName, ".")
return strings.ToLower(parts[len(parts)-1])
}
return "struct"
case reflect.Ptr:
// For pointer types, get the type of what it points to
return getTypeNameFromType(t.Elem())
default:
return "typed"
}
}

// getTypeNameFromType returns a type name from a reflect.Type
func getTypeNameFromType(t reflect.Type) string {
// Handle interface types (like any/interface{})
if t.Kind() == reflect.Interface {
if t.String() == "interface {}" {
return "any"
}
return t.String()
}

// Create a zero value and get its type name
if t.Kind() == reflect.Ptr {
// For pointer types, dereference
return getTypeNameFromType(t.Elem())
}

return getTypeName(reflect.Zero(t).Interface())
}
Loading
Loading