log: update logging option to include file output

This commit is contained in:
Tigor Hutasuhut 2024-08-11 21:45:11 +07:00
parent c0db85e85f
commit 528e408f4d
10 changed files with 166 additions and 13 deletions

3
go.mod
View file

@ -26,6 +26,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.17
github.com/ogen-go/ogen v1.2.2
github.com/rs/cors v1.11.0
github.com/samber/slog-multi v1.2.0
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
@ -63,6 +64,7 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/stephenafamo/scan v0.4.2 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
@ -77,6 +79,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.64.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

6
go.sum
View file

@ -147,6 +147,10 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/slog-multi v1.2.0 h1:JIebVdmeGkCMd5/ticlmU+aDYl4tdAZBmp5uLaSzr0k=
github.com/samber/slog-multi v1.2.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
@ -284,6 +288,8 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View file

@ -16,7 +16,7 @@ full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
kill_delay = "5s"
log = "build-errors.log"
poll = false
poll_interval = 0

1
go/.gitignore vendored
View file

@ -1,3 +1,4 @@
models/
*.db
gen/
.env

View file

@ -31,9 +31,20 @@ var Cmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := config.FromContext(ctx)
logOutput := log.WrapOsFile(os.Stderr)
prettyHandler := log.NewPrettyHandler(logOutput, nil)
slog.SetDefault(slog.New(prettyHandler))
var cleanups []func() error
defer func() {
var e []error
for _, c := range cleanups {
e = append(e, c())
}
if err := errors.Join(e...); err != nil {
fmt.Println(err.Error())
}
}()
logHandler, cleanup := log.NewHandler(cfg)
cleanups = append(cleanups, cleanup)
slog.SetDefault(slog.New(logHandler))
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"))
@ -46,6 +57,7 @@ var Cmd = &cobra.Command{
"dsn", cfg.String("db.dsn"),
)
}
cleanups = append(cleanups, sqldb.Close)
sqldb = sqldblogger.OpenDriver(
"file:data.db",
@ -98,7 +110,7 @@ var Cmd = &cobra.Command{
return errs.Wrap(err, "failed to serve")
}
slog.Info("ConnectRPC server stopped")
return errors.Join(sqldb.Close(), prettyHandler.Flush())
return nil
},
SilenceUsage: true,
}

View file

@ -26,10 +26,11 @@ func (e Entries) ToMap() map[string]any {
}
var DefaultConfig = Entries{
{"log.enable", true, `Enable console logging. If TTY is detected, a pretty logging will be used. Otherwise uses JSON format.`, false},
{"log.file.path", path.Join(xdg.CacheHome, "bluemage", "bluemage.log"), "Log file path", false},
{"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},
@ -64,6 +65,7 @@ var DefaultConfig = Entries{
{"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},
{"telemetry.service.name", "bluemage", "Name of the service to send to telemetry server", true},
{"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},

108
go/pkg/log/log.go Normal file
View file

@ -0,0 +1,108 @@
package log
import (
"errors"
"log/slog"
"net/http"
"os"
"strings"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
slogmulti "github.com/samber/slog-multi"
"github.com/tigorlazuardi/bluemage/go/config"
"github.com/tigorlazuardi/bluemage/go/pkg/telemetry"
"gopkg.in/natefinch/lumberjack.v2"
)
func NewHandler(cfg *config.Config) (slog.Handler, func() error) {
var handlers []slog.Handler
cleanup := func() error { return nil }
if cfg.Bool("log.enable") {
log, sync := createStandardLogger(cfg)
cleanup = sync
handlers = append(handlers, log)
}
if cfg.Bool("log.file.enable") {
log, clean := createFileLogger(cfg)
cl := cleanup
cleanup = func() error {
return errors.Join(cl(), clean())
}
handlers = append(handlers, log)
}
if cfg.Bool("telemetry.openobserve.enable") && cfg.Bool("telemetry.openobserve.log.enable") {
handlers = append(handlers, createO2Logger(cfg))
}
if len(handlers) == 0 {
return NullHandler{}, cleanup
}
return slogmulti.Fanout(handlers...), cleanup
}
func createFileLogger(cfg *config.Config) (slog.Handler, func() error) {
output := &lumberjack.Logger{
Filename: cfg.String("log.file.path"),
MaxSize: 15,
MaxAge: 30,
MaxBackups: 10,
LocalTime: true,
}
var lvl slog.Level
_ = lvl.UnmarshalText(cfg.Bytes("log.level"))
opts := &slog.HandlerOptions{
AddSource: cfg.Bool("log.source"),
Level: lvl,
}
return slog.NewJSONHandler(Lock(AddSync(output)), opts), output.Close
}
func createStandardLogger(cfg *config.Config) (slog.Handler, func() error) {
var output WriteSyncer
var cleanup func() error
if strings.ToLower(cfg.String("log.output")) == "stdout" {
output = Lock(AddSync(colorable.NewColorableStdout()))
cleanup = output.Sync
} else {
output = Lock(AddSync(colorable.NewColorableStderr()))
cleanup = output.Sync
}
var lvl slog.Level
_ = lvl.UnmarshalText(cfg.Bytes("log.level"))
opts := &slog.HandlerOptions{
AddSource: cfg.Bool("log.source"),
Level: lvl,
}
if isatty.IsTerminal(os.Stdout.Fd()) {
return NewPrettyHandler(output, opts), cleanup
} else {
return slog.NewJSONHandler(output, opts), cleanup
}
}
func createO2Logger(cfg *config.Config) slog.Handler {
var lvl slog.Level
_ = lvl.UnmarshalText(cfg.Bytes("telemetry.openobserve.log.level"))
opts := &slog.HandlerOptions{
AddSource: cfg.Bool("telemetry.openobserve.log.source"),
Level: lvl,
}
return telemetry.NewOpenObserveHandler(telemetry.OpenObserveHandlerOptions{
HandlerOptions: opts,
BufferSize: cfg.Int("telemetry.openobserve.log.buffer.size"),
BufferTimeout: cfg.Duration("telemetry.openobserve.log.buffer.timeout"),
Concurrency: cfg.Int("telemetry.openobserve.log.concurrency"),
Endpoint: cfg.String("telemetry.openobserve.log.endpoint"),
HTTPClient: http.DefaultClient,
Username: cfg.String("telemetry.openobserve.log.username"),
Password: cfg.String("telemetry.openobserve.log.password"),
})
}

13
go/pkg/log/null.go Normal file
View file

@ -0,0 +1,13 @@
package log
import (
"context"
"log/slog"
)
type NullHandler struct{}
func (NullHandler) Enabled(context.Context, slog.Level) bool { return false }
func (NullHandler) Handle(context.Context, slog.Record) error { return nil }
func (nu NullHandler) WithAttrs([]slog.Attr) slog.Handler { return nu }
func (nu NullHandler) WithGroup(string) slog.Handler { return nu }

View file

@ -1,8 +1,8 @@
package telemetry
import (
"go.opencensus.io/trace"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// EndWithStatus ends the span with the status of the error if not nil

View file

@ -54,12 +54,7 @@ func createProvider(ctx context.Context, cfg *config.Config) (*sdktrace.TracerPr
opts = append(opts, sdktrace.WithBatcher(o2exporter))
}
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("bluemage"),
semconv.ServiceVersionKey.String(cfg.String("runtime.version")),
attribute.String("environment", cfg.String("runtime.environment")),
)
res := NewResource(cfg)
opts = append(opts, sdktrace.WithResource(res))
@ -72,3 +67,16 @@ func (te *Telemetry) Close() error {
return te.tracer.Shutdown(ctx)
}
func NewResource(cfg *config.Config) *resource.Resource {
res, err := resource.Merge(resource.Default(), resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("bluemage"),
semconv.ServiceVersionKey.String(cfg.String("runtime.version")),
attribute.String("environment", cfg.String("runtime.environment")),
))
if err != nil {
panic(err)
}
return res
}