feat(): output hook execution logs to server logs

BREAKING CHANGES for configuration

- rename `-log-dir` to `-hook-log-dir`
- add `-hook-log-output`
- add `-log-level`
- remove `-debug`

close #44
This commit is contained in:
Nicolas Carlier 2021-05-29 15:30:02 +00:00
parent ed67fc72f6
commit 07fbb6ee3a
9 changed files with 51 additions and 19 deletions

View File

@ -26,7 +26,7 @@ $ go get -v github.com/ncarlier/webhookd
```bash ```bash
$ sudo curl -s https://raw.githubusercontent.com/ncarlier/webhookd/master/install.sh | bash $ sudo curl -s https://raw.githubusercontent.com/ncarlier/webhookd/master/install.sh | bash
or or
$ curl -sf https://gobinaries.com/ncarlier/za | sh $ curl -sf https://gobinaries.com/ncarlier/webhookd | sh
``` ```
**Or** use Docker: **Or** use Docker:
@ -204,10 +204,12 @@ $ # Retrieve logs afterwards
$ curl http://localhost:8080/echo/2 $ curl http://localhost:8080/echo/2
``` ```
If needed, you can also redirect hook logs to the server output (configured by the `WHD_HOOK_LOG_OUTPUT` environment variable).
### Post hook notifications ### Post hook notifications
The output of the script is collected and stored into a log file The output of the script is collected and stored into a log file
(configured by the `WHD_LOG_DIR` environment variable). (configured by the `WHD_HOOK_LOG_DIR` environment variable).
Once the script is executed, you can send the result and this log file to a notification channel. Once the script is executed, you can send the result and this log file to a notification channel.
Currently, only two channels are supported: `Email` and `HTTP`. Currently, only two channels are supported: `Email` and `HTTP`.
@ -255,9 +257,11 @@ The following JSON payload is POST to the target URL:
} }
``` ```
Note that because the payload have a `text` attribute, you can use a [Mattermost][mattermost] webhook endpoint. Note that because the payload have a `text` attribute, you can use a [Mattermost][mattermost], [Slack][slack] or [Discord][discord] webhook endpoint.
[mattermost]: https://docs.mattermost.com/developer/webhooks-incoming.html [mattermost]: https://docs.mattermost.com/developer/webhooks-incoming.html
[discord]: https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook
[slack]: https://api.slack.com/messaging/webhooks
#### Email notification #### Email notification

View File

@ -2,8 +2,11 @@
# Webhookd configuration # Webhookd configuration
### ###
# Output debug logs, default is false # Hook execution logs location, default is OS temporary directory
#WHD_DEBUG=false #WHD_HOOK_LOG_DIR="/tmp"
# Output hook execution logs to server logs, default is false
#WHD_HOOK_LOG_OUTPUT=false
# Maximum hook execution time in second, default is 10 # Maximum hook execution time in second, default is 10
#WHD_HOOK_TIMEOUT=10 #WHD_HOOK_TIMEOUT=10
@ -12,8 +15,8 @@
# Example: `localhost:8080` or `:8080` for all interfaces # Example: `localhost:8080` or `:8080` for all interfaces
#WHD_LISTEN_ADDR=":8080" #WHD_LISTEN_ADDR=":8080"
# Hook execution logs location, default is OS temporary directory # Log level (debug, info, warn, error), default is "info"
#WHD_LOG_DIR="/tmp" #WHD_LOG_LEVEL=info
# Number of workers to start, default is 2 # Number of workers to start, default is 2
#WHD_NB_WORKERS=2 #WHD_NB_WORKERS=2

12
main.go
View File

@ -30,14 +30,14 @@ func main() {
os.Exit(0) os.Exit(0)
} }
level := "info" if conf.HookLogOutput {
if conf.Debug { logger.Init(conf.LogLevel, "out")
level = "debug" } else {
logger.Init(conf.LogLevel)
} }
logger.Init(level)
if conf.LogDir == "" { if conf.HookLogDir == "" {
conf.LogDir = os.TempDir() conf.HookLogDir = os.TempDir()
} }
logger.Debug.Println("starting webhookd server...") logger.Debug.Println("starting webhookd server...")

View File

@ -33,7 +33,7 @@ func atoiFallback(str string, fallback int) int {
func index(conf *config.Config) http.Handler { func index(conf *config.Config) http.Handler {
defaultTimeout = conf.HookTimeout defaultTimeout = conf.HookTimeout
scriptDir = conf.ScriptDir scriptDir = conf.ScriptDir
outputDir = conf.LogDir outputDir = conf.HookLogDir
return http.HandlerFunc(webhookHandler) return http.HandlerFunc(webhookHandler)
} }

View File

@ -8,11 +8,12 @@ type Config struct {
TLSKeyFile string `flag:"tls-key-file" desc:"TLS key file" default:"server.key"` TLSKeyFile string `flag:"tls-key-file" desc:"TLS key file" default:"server.key"`
TLSDomain string `flag:"tls-domain" desc:"TLS domain name used by ACME"` TLSDomain string `flag:"tls-domain" desc:"TLS domain name used by ACME"`
NbWorkers int `flag:"nb-workers" desc:"Number of workers to start" default:"2"` NbWorkers int `flag:"nb-workers" desc:"Number of workers to start" default:"2"`
Debug bool `flag:"debug" desc:"Output debug logs" default:"false"`
HookTimeout int `flag:"hook-timeout" desc:"Maximum hook execution time in second" default:"10"` HookTimeout int `flag:"hook-timeout" desc:"Maximum hook execution time in second" default:"10"`
HookLogDir string `flag:"hook-log-dir" desc:"Hook execution logs location" default:""`
HookLogOutput bool `flag:"hook-log-output" desc:"Output hook execution logs to server logs" default:"false"`
ScriptDir string `flag:"scripts" desc:"Scripts location" default:"scripts"` ScriptDir string `flag:"scripts" desc:"Scripts location" default:"scripts"`
PasswdFile string `flag:"passwd-file" desc:"Password file for basic HTTP authentication" default:".htpasswd"` PasswdFile string `flag:"passwd-file" desc:"Password file for basic HTTP authentication" default:".htpasswd"`
LogDir string `flag:"log-dir" desc:"Hook execution logs location" default:""` LogLevel string `flag:"log-level" desc:"Log level (debug, info, warn, error)" default:"info"`
StaticDir string `flag:"static-dir" desc:"Static file directory to serve on /static path" default:""` StaticDir string `flag:"static-dir" desc:"Static file directory to serve on /static path" default:""`
NotificationURI string `flag:"notification-uri" desc:"Notification URI"` NotificationURI string `flag:"notification-uri" desc:"Notification URI"`
TrustStoreFile string `flag:"trust-store-file" desc:"Trust store used by HTTP signature verifier (.pem or .p12)"` TrustStoreFile string `flag:"trust-store-file" desc:"Trust store used by HTTP signature verifier (.pem or .p12)"`

View File

@ -43,3 +43,8 @@ func Orange(text string) string {
func Red(text string) string { func Red(text string) string {
return colorize(text, red) return colorize(text, red)
} }
// Purple ANSI color applied to a string
func Purple(text string) string {
return colorize(text, purple)
}

View File

@ -16,15 +16,18 @@ var (
Warning *log.Logger Warning *log.Logger
// Error level // Error level
Error *log.Logger Error *log.Logger
// Output special level used for script output
Output *log.Logger
) )
// Init logger level // Init logger level
func Init(level string) { func Init(level string, with ...string) {
var debugHandle, infoHandle, warnHandle, errorHandle io.Writer var debugHandle, infoHandle, warnHandle, errorHandle, outputHandle io.Writer
debugHandle = os.Stdout debugHandle = os.Stdout
infoHandle = os.Stdout infoHandle = os.Stdout
warnHandle = os.Stderr warnHandle = os.Stderr
errorHandle = os.Stderr errorHandle = os.Stderr
outputHandle = ioutil.Discard
switch level { switch level {
case "info": case "info":
debugHandle = ioutil.Discard debugHandle = ioutil.Discard
@ -37,6 +40,10 @@ func Init(level string) {
warnHandle = ioutil.Discard warnHandle = ioutil.Discard
} }
if contains(with, "out") {
outputHandle = os.Stdout
}
commonFlags := log.LstdFlags | log.Lmicroseconds commonFlags := log.LstdFlags | log.Lmicroseconds
if level == "debug" { if level == "debug" {
commonFlags = log.LstdFlags | log.Lmicroseconds | log.Lshortfile commonFlags = log.LstdFlags | log.Lmicroseconds | log.Lshortfile
@ -46,4 +53,14 @@ func Init(level string) {
Info = log.New(infoHandle, Green("INF "), commonFlags) Info = log.New(infoHandle, Green("INF "), commonFlags)
Warning = log.New(warnHandle, Orange("WRN "), commonFlags) Warning = log.New(warnHandle, Orange("WRN "), commonFlags)
Error = log.New(errorHandle, Red("ERR "), commonFlags) Error = log.New(errorHandle, Red("ERR "), commonFlags)
Output = log.New(outputHandle, Purple("OUT "), commonFlags)
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
} }

View File

@ -24,7 +24,7 @@ func printWorkMessages(work *model.WorkRequest) {
} }
func TestWorkRunner(t *testing.T) { func TestWorkRunner(t *testing.T) {
logger.Init("debug") logger.Init("debug", "out")
script := "./test_simple.sh" script := "./test_simple.sh"
args := []string{ args := []string{
"name=foo", "name=foo",

View File

@ -88,6 +88,8 @@ func Run(work *model.WorkRequest) error {
logger.Error.Printf("hook %s#%d is over ; unable to write more data into the channel: %s\n", work.Name, work.ID, line) logger.Error.Printf("hook %s#%d is over ; unable to write more data into the channel: %s\n", work.Name, work.ID, line)
break break
} }
// write to stdout if configured
logger.Output.Println(line)
// writing to outfile // writing to outfile
if _, err := wLogFile.WriteString(line + "\n"); err != nil { if _, err := wLogFile.WriteString(line + "\n"); err != nil {
logger.Error.Println("error while writing into the log file:", logFile.Name(), err) logger.Error.Println("error while writing into the log file:", logFile.Name(), err)