diff --git a/.env.example b/.env.example index effe2d7b..12753019 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ AUTO_LINK_ALBY_ACCOUNT=false # Optionally set LDK debug log level to get more info #LDK_LOG_LEVEL=5 +# Optionally set Bark debug log level to get more info +#BARK_LOG_LEVEL=5 # Optionally set Main application debug log level to get more info #LOG_LEVEL=5 diff --git a/README.md b/README.md index 1fc7303c..7d259af3 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ Bark connects to an [Ark](https://second.tech/) server. It can be configured via - `BARK_SERVER`: the Ark server URL. For signet use `https://ark.signet.2nd.dev` - `BARK_ESPLORA_SERVER`: the Esplora server URL used for chain data. For signet use `https://esplora.signet.2nd.dev`. - `BARK_SERVER_ACCESS_TOKEN`: an optional access token required by the Ark server (pre-public mainnet launch). +- `BARK_LOG_LEVEL`: Log level for Bark. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose Bark logging (e.g., level 4 or 5) without enabling verbose logging for the entire application. ### Alby OAuth diff --git a/config/models.go b/config/models.go index 4c3b7816..ca0be744 100644 --- a/config/models.go +++ b/config/models.go @@ -67,6 +67,7 @@ type AppConfig struct { BarkServer string `envconfig:"BARK_SERVER" default:"https://ark.second.tech"` BarkEsploraServer string `envconfig:"BARK_ESPLORA_SERVER" default:"https://mempool.second.tech/api"` BarkServerAccessToken string `envconfig:"BARK_SERVER_ACCESS_TOKEN"` + BarkLogLevel string `envconfig:"BARK_LOG_LEVEL" default:"3"` } func (c *AppConfig) IsDefaultClientId() bool { diff --git a/lnclient/bark/bark.go b/lnclient/bark/bark.go index 57b4f209..23947b6e 100644 --- a/lnclient/bark/bark.go +++ b/lnclient/bark/bark.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "os" + "strconv" "strings" "sync" "time" @@ -50,6 +51,12 @@ type Config struct { // ServerAccessToken is an optional access token required by some Ark // servers (currently used to gate mainnet access ahead of a public launch). ServerAccessToken string + // LogLevel is the logrus level (as an int string, e.g. "3" for Info) used + // for bark's own internal logs. Defaults to Info if empty/unparseable. + LogLevel string + // LogToFile controls whether bark's logs are also written to a dedicated + // bark.log file alongside the other backend logs. + LogToFile bool } type BarkService struct { @@ -103,6 +110,14 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w return nil, err } + // Forward bark's internal logs into a dedicated logger. Done before opening + // the wallet so any logs emitted during open are captured. + logLevel, err := strconv.Atoi(config.LogLevel) + if err != nil { + logLevel = int(logrus.InfoLevel) + } + installBarkLogger(logrus.Level(logLevel), config.LogToFile, workDir) + // Usually, you have two wait 2 blocks. You can set nb_min_round_confirmations=0 to make it go faster. roundTxRequiredConfirmations := uint32(0) diff --git a/lnclient/bark/bark_unsupported.go b/lnclient/bark/bark_unsupported.go index 81842341..cb8bb794 100644 --- a/lnclient/bark/bark_unsupported.go +++ b/lnclient/bark/bark_unsupported.go @@ -22,6 +22,8 @@ type Config struct { ServerAddress string EsploraAddress string ServerAccessToken string + LogLevel string + LogToFile bool } func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, workDir, mnemonic string, config Config) (lnclient.LNClient, error) { diff --git a/lnclient/bark/logger.go b/lnclient/bark/logger.go new file mode 100644 index 00000000..fa7d88da --- /dev/null +++ b/lnclient/bark/logger.go @@ -0,0 +1,123 @@ +//go:build (darwin && (amd64 || arm64)) || (linux && (amd64 || arm64)) || (windows && amd64) + +package bark + +import ( + "os" + "path/filepath" + "sync" + + "github.com/orandin/lumberjackrus" + "github.com/sirupsen/logrus" + bark "gitlab.com/ark-bitcoin/bark-ffi-bindings/golang/bark" + + "github.com/getAlby/hub/logger" +) + +const barkLogFilename = "bark.log" +const logDir = "log" + +// setLoggerOnce guards bark.SetLogger, which can only be installed once per +// process (calling it again returns an error from the underlying bridge). +var setLoggerOnce sync.Once + +// barkLogger forwards bark's internal log records to a dedicated logrus logger +// (separate from the main app logger, with its own level and log file) so bark +// logs can be tuned independently of the rest of the hub. +type barkLogger struct { + logger *logrus.Logger +} + +var _ bark.BarkLogger = &barkLogger{} + +// Log implements bark.BarkLogger. The record's bark log level is mapped onto the +// equivalent logrus level, and the bark target is attached as a structured field. +// +// IMPORTANT: this must not call back into any bark API that itself emits log +// records, or the foreign runtime may stack-overflow or deadlock. +func (l *barkLogger) Log(level bark.LogLevel, target string, message string) { + l.logger.WithFields(logrus.Fields{ + "log_type": "bark", + "target": target, + }).Log(barkLevelToLogrus(level), message) +} + +func barkLevelToLogrus(level bark.LogLevel) logrus.Level { + switch level { + case bark.LogLevelError: + return logrus.ErrorLevel + case bark.LogLevelWarn: + return logrus.WarnLevel + case bark.LogLevelInfo: + return logrus.InfoLevel + case bark.LogLevelDebug: + return logrus.DebugLevel + case bark.LogLevelTrace: + return logrus.TraceLevel + } + logger.Logger.WithField("log_level", level).Error("Unknown Bark log level") + return logrus.ErrorLevel +} + +// logrusToBarkLevel maps a logrus level onto the bark log level used to cap +// what the FFI bridge emits, so filtered-out records don't cross the boundary. +func logrusToBarkLevel(level logrus.Level) bark.LogLevel { + switch level { + case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel: + return bark.LogLevelError + case logrus.WarnLevel: + return bark.LogLevelWarn + case logrus.InfoLevel: + return bark.LogLevelInfo + case logrus.DebugLevel: + return bark.LogLevelDebug + default: + return bark.LogLevelTrace + } +} + +// installBarkLogger wires bark's internal logs into a dedicated logrus logger. +// It is safe to call more than once (e.g. when reopening the wallet); only the +// first call installs the logger, as the bridge can only be set once per +// process. +func installBarkLogger(logLevel logrus.Level, logToFile bool, workDir string) { + setLoggerOnce.Do(func() { + barkLogrus, err := createBarkLogger(logLevel, logToFile, workDir) + if err != nil { + logger.Logger.WithError(err).Error("Failed to create Bark logger") + return + } + // Cap the FFI bridge at the configured level so records below it are + // never produced; the dedicated logger applies the same level again. + if err := bark.SetLogger(&barkLogger{logger: barkLogrus}, logrusToBarkLevel(logLevel)); err != nil { + logger.Logger.WithError(err).Warn("Failed to install Bark logger") + } + }) +} + +func createBarkLogger(logLevel logrus.Level, logToFile bool, workDir string) (*logrus.Logger, error) { + barkLogrus := logrus.New() + barkLogrus.SetFormatter(&logrus.JSONFormatter{}) + barkLogrus.SetOutput(os.Stdout) + barkLogrus.SetLevel(logLevel) + + if logToFile { + parentDir := filepath.Dir(workDir) + barkLogFilePath := filepath.Join(parentDir, logDir, barkLogFilename) + barkFileLoggerHook, err := lumberjackrus.NewHook( + &lumberjackrus.LogFile{ + Filename: barkLogFilePath, + MaxAge: 3, + MaxBackups: 3, + }, + logLevel, + &logrus.JSONFormatter{}, + nil, + ) + if err != nil { + return nil, err + } + barkLogrus.AddHook(barkFileLoggerHook) + } + return barkLogrus, nil +} diff --git a/service/start.go b/service/start.go index e18d35f3..16a9e4fc 100644 --- a/service/start.go +++ b/service/start.go @@ -384,6 +384,8 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e ServerAddress: env.BarkServer, EsploraAddress: env.BarkEsploraServer, ServerAccessToken: env.BarkServerAccessToken, + LogLevel: env.BarkLogLevel, + LogToFile: env.LogToFile, }) case config.CLNBackendType: CLNAddress, _ := svc.cfg.Get("CLNAddress", encryptionKey)