go: preparing the CLI interface
This commit is contained in:
parent
b94b9e7d20
commit
c0db85e85f
1
go.mod
1
go.mod
|
@ -14,6 +14,7 @@ require (
|
|||
github.com/go-faster/errors v0.7.1
|
||||
github.com/go-faster/jx v1.1.0
|
||||
github.com/jaswdr/faker/v2 v2.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/knadh/koanf/parsers/json v0.1.0
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -98,6 +98,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jaswdr/faker/v2 v2.3.0 h1:jgQ9UmU2Eb5tSQ8JkUS4tPoyTM2OtThQpOpwk7Fa9RY=
|
||||
github.com/jaswdr/faker/v2 v2.3.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU=
|
||||
|
|
|
@ -2,11 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tigorlazuardi/bluemage/go/cmd/bluemage/serve"
|
||||
"github.com/tigorlazuardi/bluemage/go/config"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
|
@ -16,12 +21,54 @@ var Cmd = &cobra.Command{
|
|||
|
||||
func init() {
|
||||
Cmd.AddCommand(serve.Cmd)
|
||||
|
||||
flags := Cmd.PersistentFlags()
|
||||
for _, entry := range config.DefaultConfig {
|
||||
key, value, desc, hidden := entry.Key, entry.Value, entry.Desc, entry.Hidden
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
flags.Bool(key, v, desc)
|
||||
case string:
|
||||
flags.String(key, v, desc)
|
||||
case int:
|
||||
flags.Int(key, v, desc)
|
||||
case float32:
|
||||
flags.Float32(key, v, desc)
|
||||
case float64:
|
||||
flags.Float64(key, v, desc)
|
||||
default:
|
||||
flags.String(key, fmt.Sprintf("%v", v), desc)
|
||||
}
|
||||
if hidden {
|
||||
flags.MarkHidden(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
xdgJson, _ := xdg.ConfigFile("bluemage/config.json")
|
||||
xdgYaml, _ := xdg.ConfigFile("bluemage/config.yaml")
|
||||
|
||||
cfg, err := config.NewConfigBuilder().
|
||||
LoadDefault().
|
||||
LoadJSONFile("/etc/bluemage/config.json").
|
||||
LoadYamlFile("/etc/bluemage/config.yaml").
|
||||
LoadJSONFile(xdgJson).
|
||||
LoadYamlFile(xdgYaml).
|
||||
LoadJSONFile("config.json").
|
||||
LoadYamlFile("config.yaml").
|
||||
LoadEnv().
|
||||
LoadFlags(Cmd.PersistentFlags()).
|
||||
BuildHandle()
|
||||
if err != nil {
|
||||
slog.Error("BLUEMAGE: failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
ctx = config.WithContext(ctx, cfg)
|
||||
if err := Cmd.ExecuteContext(ctx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
@ -15,6 +17,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/tigorlazuardi/bluemage/go/api"
|
||||
"github.com/tigorlazuardi/bluemage/go/config"
|
||||
"github.com/tigorlazuardi/bluemage/go/gen/proto/device/v1/v1connect"
|
||||
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
||||
|
@ -26,9 +29,22 @@ import (
|
|||
var Cmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sqldb, err := sql.Open("sqlite3", "file:data.db")
|
||||
ctx := cmd.Context()
|
||||
cfg := config.FromContext(ctx)
|
||||
logOutput := log.WrapOsFile(os.Stderr)
|
||||
prettyHandler := log.NewPrettyHandler(logOutput, nil)
|
||||
slog.SetDefault(slog.New(prettyHandler))
|
||||
|
||||
if err := os.MkdirAll(cfg.String("download.directory"), 0755); err != nil {
|
||||
return errs.Wrapw(err, "failed to create download directory", "path", cfg.String("download.directory"))
|
||||
}
|
||||
|
||||
sqldb, err := sql.Open(cfg.String("db.driver"), fmt.Sprintf("file:%s", cfg.String("db.path")))
|
||||
if err != nil {
|
||||
return errs.Wrapw(err, "failed to open database", "file", "data.db")
|
||||
return errs.Wrapw(err, "failed to open database",
|
||||
"driver", cfg.String("db.driver"),
|
||||
"dsn", cfg.String("db.dsn"),
|
||||
)
|
||||
}
|
||||
|
||||
sqldb = sqldblogger.OpenDriver(
|
||||
|
@ -38,9 +54,6 @@ var Cmd = &cobra.Command{
|
|||
sqldblogger.WithSQLQueryAsMessage(true),
|
||||
)
|
||||
db := bob.New(sqldb)
|
||||
logOutput := log.WrapOsFile(os.Stderr)
|
||||
prettyHandler := log.NewPrettyHandler(logOutput, nil)
|
||||
slog.SetDefault(slog.New(prettyHandler))
|
||||
|
||||
api := &api.API{
|
||||
DB: db,
|
||||
|
@ -64,12 +77,15 @@ var Cmd = &cobra.Command{
|
|||
)))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Addr: fmt.Sprintf("%s:%s", cfg.String("http.host"), cfg.String("http.port")),
|
||||
Handler: h2c.NewHandler(server.WithCORS(mux), &http2.Server{}),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-cmd.Context().Done()
|
||||
<-ctx.Done()
|
||||
slog.Info("Exit signal received. Shutting down server")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
@ -86,3 +102,6 @@ var Cmd = &cobra.Command{
|
|||
},
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
func Register(cmd *cobra.Command, config *config.Config) {
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (builder *ConfigBuilder) BuildHandle() (*Config, error) {
|
|||
}
|
||||
|
||||
func (builder *ConfigBuilder) LoadDefault() *ConfigBuilder {
|
||||
provider := confmap.Provider(DefaultConfig, ".")
|
||||
provider := confmap.Provider(DefaultConfig.ToMap(), ".")
|
||||
|
||||
err := builder.koanf.Load(provider, nil)
|
||||
if err != nil {
|
||||
|
|
20
go/config/context.go
Normal file
20
go/config/context.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package config
|
||||
|
||||
import "context"
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
var key = contextKey{}
|
||||
|
||||
// WithContext returns a new context with the given Config.
|
||||
func WithContext(ctx context.Context, cfg *Config) context.Context {
|
||||
return context.WithValue(ctx, key, cfg)
|
||||
}
|
||||
|
||||
// FromContext returns the Config from the given context.
|
||||
//
|
||||
// Returns nil if the Config is not found.
|
||||
func FromContext(ctx context.Context) *Config {
|
||||
cfg, _ := ctx.Value(key).(*Config)
|
||||
return cfg
|
||||
}
|
|
@ -2,61 +2,69 @@ package config
|
|||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
var Version string = "unknown"
|
||||
|
||||
var DefaultConfig = map[string]any{
|
||||
"flags.containerized": false,
|
||||
|
||||
"log.enable": true,
|
||||
"log.level": "info",
|
||||
"log.output": "stderr",
|
||||
"log.file.enable": true,
|
||||
"log.file.path": path.Join(xdg.CacheHome, "bluemage", "bluemage.log"),
|
||||
|
||||
"db.driver": "sqlite3",
|
||||
"db.string": path.Join(xdg.Home, ".local", "share", "bluemage", "data.db"),
|
||||
|
||||
"pubsub.db.name": path.Join(xdg.Home, ".local", "share", "bluemage", "pubsub.db"),
|
||||
"pubsub.db.timeout": "5s",
|
||||
"pubsub.ack.deadline": "30m",
|
||||
|
||||
"download.concurrency.images": 5,
|
||||
"download.concurrency.subreddits": 3,
|
||||
|
||||
"download.directory": path.Join(xdg.UserDirs.Pictures, "bluemage"),
|
||||
"download.timeout.headers": "10s",
|
||||
"download.timeout.idle": "5s",
|
||||
"download.timeout.idlespeed": "10KB",
|
||||
"download.useragent": "bluemage/v1",
|
||||
|
||||
"http.port": "8080",
|
||||
"http.host": "0.0.0.0",
|
||||
"http.shutdown_timeout": "5s",
|
||||
|
||||
"telemetry.openobserve.enable": false,
|
||||
"telemetry.openobserve.log.enable": true,
|
||||
"telemetry.openobserve.log.level": "info",
|
||||
"telemetry.openobserve.log.source": true,
|
||||
"telemetry.openobserve.log.endpoint": "http://localhost:5080/api/default/default/_json",
|
||||
"telemetry.openobserve.log.concurrency": 4,
|
||||
"telemetry.openobserve.log.buffer.size": 2 * 1024, // 2kb
|
||||
"telemetry.openobserve.log.buffer.timeout": "2s",
|
||||
"telemetry.openobserve.log.username": "root@example.com",
|
||||
"telemetry.openobserve.log.password": "Complexpass#123",
|
||||
|
||||
"telemetry.openobserve.trace.enable": true,
|
||||
"telemetry.openobserve.trace.url": "http://localhost:5080/api/default/v1/traces",
|
||||
"telemetry.openobserve.trace.auth": "Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
|
||||
"telemetry.trace.ratio": 1,
|
||||
|
||||
"runtime.version": Version,
|
||||
"runtime.environment": "development",
|
||||
|
||||
"scheduler.timeout": time.Second * 10,
|
||||
type Entry struct {
|
||||
Key string
|
||||
Value any
|
||||
Desc string
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
type Entries []Entry
|
||||
|
||||
func (e Entries) ToMap() map[string]any {
|
||||
m := make(map[string]any, len(e))
|
||||
for _, entry := range e {
|
||||
m[entry.Key] = entry.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var DefaultConfig = Entries{
|
||||
{"log.file.enable", true, "Enable file logging", false},
|
||||
{"log.level", "info", `Log level. Possible values: "debug", "info", "warn", "error"`, false},
|
||||
{"log.output", "stderr", "Log output. Possible values: \"stdout\", \"stderr\"", false},
|
||||
{"log.file.path", path.Join(xdg.CacheHome, "bluemage", "bluemage.log"), "Log file path", false},
|
||||
|
||||
{"db.driver", "sqlite3", "Database driver", false},
|
||||
{"db.path", path.Join(xdg.Home, ".local", "share", "bluemage", "data.db"), "Database path", false},
|
||||
|
||||
{"pubsub.db.path", path.Join(xdg.Home, ".local", "share", "bluemage", "pubsub.db"), "PubSub database path", false},
|
||||
{"pubsub.db.timeout", "5s", "PubSub database timeout", false},
|
||||
{"pubsub.ack.deadline", "30m", "PubSub ack deadline", false},
|
||||
|
||||
{"download.concurrency", 5, "Number of concurrent image downloads", false},
|
||||
{"download.directory", path.Join(xdg.UserDirs.Pictures, "bluemage"), "Download directory", false},
|
||||
{"download.timeout.headers", "10s", "How long to wait for Reddit to response with data after opening connection. To prevent download connection getting clogged up for too long", false},
|
||||
{"download.timeout.idle", "5s", "Download idle timeout. Whenever a download speed is under idlespeed for this duration, the download is cancelled. To prevent slow download speed from clogging up the queue", false},
|
||||
{"download.timeout.idlespeed", "10KB", "Threshold for the download speed to be considered 'idle'", false},
|
||||
{"download.useragent", "bluemage/v1", "Download user agent. Must not be empty. Avoid common user agent values like 'curl'", false},
|
||||
|
||||
{"http.port", "8080", "HTTP server port", false},
|
||||
{"http.host", "0.0.0.0", "HTTP server host", false},
|
||||
{"http.shutdown_timeout", "5s", "Timeout to wait closing i/o connections until the server force closes everything", false},
|
||||
|
||||
{"telemetry.openobserve.enable", false, "Wether to enable telemetry support to OpenObserve server. Setting this to false, disables all other 'telemetry.openobserve' options (default false)", false},
|
||||
{"telemetry.openobserve.log.enable", true, "Wether to enable logging telemetry to OpenObserve server", false},
|
||||
{"telemetry.openobserve.log.level", "info", "Log level for telemetry logging", false},
|
||||
{"telemetry.openobserve.log.source", true, "Wether to include source code in the log", false},
|
||||
{"telemetry.openobserve.log.endpoint", "http://localhost:5080/api/default/default/_json", "OpenObserve log endpoint", false},
|
||||
{"telemetry.openobserve.log.concurrency", 4, "Number of allowed concurrent connections to send logs to the server", false},
|
||||
{"telemetry.openobserve.log.buffer.size", 2 * 1024, "Buffer size (in bytes) to wait before being sent to the server", false},
|
||||
{"telemetry.openobserve.log.buffer.timeout", "2s", "Timeout to send logs if the buffer is not empty but not yet filled", false},
|
||||
{"telemetry.openobserve.log.username", "root@example.com", "OpenObserve logging endpoint username", false},
|
||||
{"telemetry.openobserve.log.password", "Complexpass#123", "OpenObserve logging endpoint password", false},
|
||||
{"telemetry.openobserve.trace.enable", true, "Wether to enable sending tracing to OpenObserve server", false},
|
||||
{"telemetry.openobserve.trace.url", "http://localhost:5080/api/default/v1/traces", "Endpoint url to send traces to", false},
|
||||
{"telemetry.openobserve.trace.auth", "Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "Authorization token for the Endpoint URL", false},
|
||||
{"telemetry.trace.ratio", float64(1), "Sampling ratio between 0 to 1 on how many traces are sent to the server. Value of 1 will send everything", false},
|
||||
|
||||
{"runtime.version", Version, "current server version", true},
|
||||
{"runtime.environment", "development", "runtime environment", true},
|
||||
{"flags.containerized", false, "Indicates the application is running in a container", true},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue