diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index eb555667c..49cbd7bbc 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -4,6 +4,10 @@ fragment ConfigGeneralData on ConfigGeneralResult { generatedPath username password + logFile + logOut + logLevel + logAccess } fragment ConfigInterfaceData on ConfigInterfaceResult { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 2ff1999d0..d126354bf 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -9,6 +9,14 @@ input ConfigGeneralInput { username: String """Password""" password: String + """Name of the log file""" + logFile: String + """Whether to also output to stderr""" + logOut: Boolean! + """Minimum log level""" + logLevel: String! + """Whether to log http access""" + logAccess: Boolean! } type ConfigGeneralResult { @@ -22,6 +30,14 @@ type ConfigGeneralResult { username: String! """Password""" password: String! + """Name of the log file""" + logFile: String + """Whether to also output to stderr""" + logOut: Boolean! + """Minimum log level""" + logLevel: String! + """Whether to log http access""" + logAccess: Boolean! } input ConfigInterfaceInput { diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 53a205cbd..4ca980ca6 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/stashapp/stash/pkg/manager/config" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) @@ -50,6 +51,18 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co } } + if input.LogFile != nil { + config.Set(config.LogFile, input.LogFile) + } + + config.Set(config.LogOut, input.LogOut) + config.Set(config.LogAccess, input.LogAccess) + + if input.LogLevel != config.GetLogLevel() { + config.Set(config.LogLevel, input.LogLevel) + logger.SetLogLevel(input.LogLevel) + } + if err := config.Write(); err != nil { return makeConfigGeneralResult(), err } diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index 2eb9e86d7..ae8e07d4c 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -28,12 +28,17 @@ func makeConfigResult() *models.ConfigResult { } func makeConfigGeneralResult() *models.ConfigGeneralResult { + logFile := config.GetLogFile() return &models.ConfigGeneralResult{ Stashes: config.GetStashPaths(), DatabasePath: config.GetDatabasePath(), GeneratedPath: config.GetGeneratedPath(), Username: config.GetUsername(), Password: config.GetPasswordHash(), + LogFile: &logFile, + LogOut: config.GetLogOut(), + LogLevel: config.GetLogLevel(), + LogAccess: config.GetLogAccess(), } } diff --git a/pkg/api/server.go b/pkg/api/server.go index baa3c1dfe..83554b969 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -72,7 +72,10 @@ func Start() { r.Use(authenticateHandler()) r.Use(middleware.Recoverer) - r.Use(middleware.Logger) + + if config.GetLogAccess() { + r.Use(middleware.Logger) + } r.Use(middleware.DefaultCompress) r.Use(middleware.StripSlashes) r.Use(cors.AllowAll().Handler) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 1789c56a5..72fae7616 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,6 +2,8 @@ package logger import ( "fmt" + "io" + "os" "sync" "time" @@ -24,6 +26,50 @@ var waiting = false var lastBroadcast = time.Now() var logBuffer []LogItem +// Init initialises the logger based on a logging configuration +func Init(logFile string, logOut bool, logLevel string) { + var file *os.File + + if logFile != "" { + var err error + file, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + + if err != nil { + fmt.Printf("Could not open '%s' for log output due to error: %s\n", logFile, err.Error()) + logFile = "" + } + } + + if file != nil && logOut { + mw := io.MultiWriter(os.Stderr, file) + logger.Out = mw + } else if file != nil { + logger.Out = file + } + + // otherwise, output to StdErr + + SetLogLevel(logLevel) +} + +func SetLogLevel(level string) { + logger.Level = logLevelFromString(level) +} + +func logLevelFromString(level string) logrus.Level { + ret := logrus.InfoLevel + + if level == "Debug" { + ret = logrus.DebugLevel + } else if level == "Warning" { + ret = logrus.WarnLevel + } else if level == "Error" { + ret = logrus.ErrorLevel + } + + return ret +} + func addLogItem(l *LogItem) { mutex.Lock() l.Time = time.Now() diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index f66c34f2a..26e376cad 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -4,6 +4,7 @@ import ( "golang.org/x/crypto/bcrypt" "io/ioutil" + "github.com/spf13/viper" "github.com/stashapp/stash/pkg/utils" @@ -24,6 +25,12 @@ const Port = "port" const CSSEnabled = "cssEnabled" +// Logging options +const LogFile = "logFile" +const LogOut = "logOut" +const LogLevel = "logLevel" +const LogAccess = "logAccess" + func Set(key string, value interface{}) { viper.Set(key, value) } @@ -155,6 +162,48 @@ func GetCSSEnabled() bool { return viper.GetBool(CSSEnabled) } +// GetLogFile returns the filename of the file to output logs to. +// An empty string means that file logging will be disabled. +func GetLogFile() string { + return viper.GetString(LogFile) +} + +// GetLogOut returns true if logging should be output to the terminal +// in addition to writing to a log file. Logging will be output to the +// terminal if file logging is disabled. Defaults to true. +func GetLogOut() bool { + ret := true + if viper.IsSet(LogOut) { + ret = viper.GetBool(LogOut) + } + + return ret +} + +// GetLogLevel returns the lowest log level to write to the log. +// Should be one of "Debug", "Info", "Warning", "Error" +func GetLogLevel() string { + const defaultValue = "Info" + + value := viper.GetString(LogLevel) + if value != "Debug" && value != "Info" && value != "Warning" && value != "Error" { + value = defaultValue + } + + return value +} + +// GetLogAccess returns true if http requests should be logged to the terminal. +// HTTP requests are not logged to the log file. Defaults to true. +func GetLogAccess() bool { + ret := true + if viper.IsSet(LogAccess) { + ret = viper.GetBool(LogAccess) + } + + return ret +} + func IsValid() bool { setPaths := viper.IsSet(Stash) && viper.IsSet(Cache) && viper.IsSet(Generated) && viper.IsSet(Metadata) diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index c2b7a316d..bb21d76ca 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -34,6 +34,7 @@ func Initialize() *singleton { once.Do(func() { _ = utils.EnsureDir(paths.GetConfigDirectory()) initConfig() + initLog() initFlags() initEnvs() instance = &singleton{ @@ -126,6 +127,10 @@ The error was: %s instance.FFProbePath = ffprobePath } +func initLog() { + logger.Init(config.GetLogFile(), config.GetLogOut(), config.GetLogLevel()) +} + func (s *singleton) refreshConfig() { s.Paths = paths.NewPaths() if config.IsValid() { diff --git a/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx index 101459245..06c7e86be 100644 --- a/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx +++ b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx @@ -8,12 +8,13 @@ import { InputGroup, Spinner, Tag, + Checkbox, + HTMLSelect, } from "@blueprintjs/core"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as GQL from "../../core/generated-graphql"; import { StashService } from "../../core/StashService"; import { ErrorUtils } from "../../utils/errors"; -import { TextUtils } from "../../utils/text"; import { ToastUtils } from "../../utils/toasts"; import { FolderSelect } from "../Shared/FolderSelect/FolderSelect"; @@ -26,6 +27,10 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr const [generatedPath, setGeneratedPath] = useState(undefined); const [username, setUsername] = useState(undefined); const [password, setPassword] = useState(undefined); + const [logFile, setLogFile] = useState(); + const [logOut, setLogOut] = useState(true); + const [logLevel, setLogLevel] = useState("Info"); + const [logAccess, setLogAccess] = useState(true); const { data, error, loading } = StashService.useConfiguration(); @@ -35,6 +40,10 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr generatedPath, username, password, + logFile, + logOut, + logLevel, + logAccess, }); useEffect(() => { @@ -46,6 +55,10 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr setGeneratedPath(conf.general.generatedPath); setUsername(conf.general.username); setPassword(conf.general.password); + setLogFile(conf.general.logFile); + setLogOut(conf.general.logOut); + setLogLevel(conf.general.logLevel); + setLogAccess(conf.general.logAccess); } }, [data]); @@ -89,6 +102,9 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr > setGeneratedPath(e.target.value)} /> + + +

Authentication

= (props: IPr > setPassword(e.target.value)} /> + + +

Logging

+ + setLogFile(e.target.value)} /> + + + + setLogOut(!logOut)} + /> + + + + setLogLevel(event.target.value)} + value={logLevel} + /> + + + + setLogAccess(!logAccess)} + /> + +