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/errors v0.7.1
|
||||||
github.com/go-faster/jx v1.1.0
|
github.com/go-faster/jx v1.1.0
|
||||||
github.com/jaswdr/faker/v2 v2.3.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/json v0.1.0
|
||||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||||
github.com/knadh/koanf/providers/confmap 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/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 h1:jgQ9UmU2Eb5tSQ8JkUS4tPoyTM2OtThQpOpwk7Fa9RY=
|
||||||
github.com/jaswdr/faker/v2 v2.3.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q=
|
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 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||||
github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU=
|
github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU=
|
||||||
|
|
|
@ -2,11 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/tigorlazuardi/bluemage/go/cmd/bluemage/serve"
|
"github.com/tigorlazuardi/bluemage/go/cmd/bluemage/serve"
|
||||||
|
"github.com/tigorlazuardi/bluemage/go/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
var Cmd = &cobra.Command{
|
||||||
|
@ -16,12 +21,54 @@ var Cmd = &cobra.Command{
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Cmd.AddCommand(serve.Cmd)
|
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() {
|
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)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
ctx = config.WithContext(ctx, cfg)
|
||||||
if err := Cmd.ExecuteContext(ctx); err != nil {
|
if err := Cmd.ExecuteContext(ctx); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
@ -15,6 +17,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
"github.com/tigorlazuardi/bluemage/go/api"
|
"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/gen/proto/device/v1/v1connect"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
"github.com/tigorlazuardi/bluemage/go/pkg/errs"
|
||||||
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
"github.com/tigorlazuardi/bluemage/go/pkg/log"
|
||||||
|
@ -26,9 +29,22 @@ import (
|
||||||
var Cmd = &cobra.Command{
|
var Cmd = &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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 {
|
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(
|
sqldb = sqldblogger.OpenDriver(
|
||||||
|
@ -38,9 +54,6 @@ var Cmd = &cobra.Command{
|
||||||
sqldblogger.WithSQLQueryAsMessage(true),
|
sqldblogger.WithSQLQueryAsMessage(true),
|
||||||
)
|
)
|
||||||
db := bob.New(sqldb)
|
db := bob.New(sqldb)
|
||||||
logOutput := log.WrapOsFile(os.Stderr)
|
|
||||||
prettyHandler := log.NewPrettyHandler(logOutput, nil)
|
|
||||||
slog.SetDefault(slog.New(prettyHandler))
|
|
||||||
|
|
||||||
api := &api.API{
|
api := &api.API{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -64,12 +77,15 @@ var Cmd = &cobra.Command{
|
||||||
)))
|
)))
|
||||||
|
|
||||||
server := &http.Server{
|
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{}),
|
Handler: h2c.NewHandler(server.WithCORS(mux), &http2.Server{}),
|
||||||
|
BaseContext: func(net.Listener) context.Context {
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-cmd.Context().Done()
|
<-ctx.Done()
|
||||||
slog.Info("Exit signal received. Shutting down server")
|
slog.Info("Exit signal received. Shutting down server")
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -86,3 +102,6 @@ var Cmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
SilenceUsage: true,
|
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 {
|
func (builder *ConfigBuilder) LoadDefault() *ConfigBuilder {
|
||||||
provider := confmap.Provider(DefaultConfig, ".")
|
provider := confmap.Provider(DefaultConfig.ToMap(), ".")
|
||||||
|
|
||||||
err := builder.koanf.Load(provider, nil)
|
err := builder.koanf.Load(provider, nil)
|
||||||
if err != 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 (
|
import (
|
||||||
"path"
|
"path"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version string = "unknown"
|
var Version string = "unknown"
|
||||||
|
|
||||||
var DefaultConfig = map[string]any{
|
type Entry struct {
|
||||||
"flags.containerized": false,
|
Key string
|
||||||
|
Value any
|
||||||
"log.enable": true,
|
Desc string
|
||||||
"log.level": "info",
|
Hidden bool
|
||||||
"log.output": "stderr",
|
}
|
||||||
"log.file.enable": true,
|
|
||||||
"log.file.path": path.Join(xdg.CacheHome, "bluemage", "bluemage.log"),
|
type Entries []Entry
|
||||||
|
|
||||||
"db.driver": "sqlite3",
|
func (e Entries) ToMap() map[string]any {
|
||||||
"db.string": path.Join(xdg.Home, ".local", "share", "bluemage", "data.db"),
|
m := make(map[string]any, len(e))
|
||||||
|
for _, entry := range e {
|
||||||
"pubsub.db.name": path.Join(xdg.Home, ".local", "share", "bluemage", "pubsub.db"),
|
m[entry.Key] = entry.Value
|
||||||
"pubsub.db.timeout": "5s",
|
}
|
||||||
"pubsub.ack.deadline": "30m",
|
return m
|
||||||
|
}
|
||||||
"download.concurrency.images": 5,
|
|
||||||
"download.concurrency.subreddits": 3,
|
var DefaultConfig = Entries{
|
||||||
|
{"log.file.enable", true, "Enable file logging", false},
|
||||||
"download.directory": path.Join(xdg.UserDirs.Pictures, "bluemage"),
|
{"log.level", "info", `Log level. Possible values: "debug", "info", "warn", "error"`, false},
|
||||||
"download.timeout.headers": "10s",
|
{"log.output", "stderr", "Log output. Possible values: \"stdout\", \"stderr\"", false},
|
||||||
"download.timeout.idle": "5s",
|
{"log.file.path", path.Join(xdg.CacheHome, "bluemage", "bluemage.log"), "Log file path", false},
|
||||||
"download.timeout.idlespeed": "10KB",
|
|
||||||
"download.useragent": "bluemage/v1",
|
{"db.driver", "sqlite3", "Database driver", false},
|
||||||
|
{"db.path", path.Join(xdg.Home, ".local", "share", "bluemage", "data.db"), "Database path", false},
|
||||||
"http.port": "8080",
|
|
||||||
"http.host": "0.0.0.0",
|
{"pubsub.db.path", path.Join(xdg.Home, ".local", "share", "bluemage", "pubsub.db"), "PubSub database path", false},
|
||||||
"http.shutdown_timeout": "5s",
|
{"pubsub.db.timeout", "5s", "PubSub database timeout", false},
|
||||||
|
{"pubsub.ack.deadline", "30m", "PubSub ack deadline", false},
|
||||||
"telemetry.openobserve.enable": false,
|
|
||||||
"telemetry.openobserve.log.enable": true,
|
{"download.concurrency", 5, "Number of concurrent image downloads", false},
|
||||||
"telemetry.openobserve.log.level": "info",
|
{"download.directory", path.Join(xdg.UserDirs.Pictures, "bluemage"), "Download directory", false},
|
||||||
"telemetry.openobserve.log.source": true,
|
{"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},
|
||||||
"telemetry.openobserve.log.endpoint": "http://localhost:5080/api/default/default/_json",
|
{"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},
|
||||||
"telemetry.openobserve.log.concurrency": 4,
|
{"download.timeout.idlespeed", "10KB", "Threshold for the download speed to be considered 'idle'", false},
|
||||||
"telemetry.openobserve.log.buffer.size": 2 * 1024, // 2kb
|
{"download.useragent", "bluemage/v1", "Download user agent. Must not be empty. Avoid common user agent values like 'curl'", false},
|
||||||
"telemetry.openobserve.log.buffer.timeout": "2s",
|
|
||||||
"telemetry.openobserve.log.username": "root@example.com",
|
{"http.port", "8080", "HTTP server port", false},
|
||||||
"telemetry.openobserve.log.password": "Complexpass#123",
|
{"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.trace.enable": true,
|
|
||||||
"telemetry.openobserve.trace.url": "http://localhost:5080/api/default/v1/traces",
|
{"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.trace.auth": "Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
{"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.trace.ratio": 1,
|
{"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},
|
||||||
"runtime.version": Version,
|
{"telemetry.openobserve.log.concurrency", 4, "Number of allowed concurrent connections to send logs to the server", false},
|
||||||
"runtime.environment": "development",
|
{"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},
|
||||||
"scheduler.timeout": time.Second * 10,
|
{"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