Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions cmd/hookah/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package main
import (
_ "embed"
"flag"
"log"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"strconv"
Expand All @@ -19,8 +21,14 @@ var (
secret = flag.String("secret", "", "Optional GitHub HMAC secret key")
timeout = flag.Duration("timeout", 10*time.Minute, "Exec timeout on hook scripts")
verbose = flag.Bool("v", false, "Enable verbose logger output")
)

var (
errlog = flag.String("err-log", "", "Path to write the error log to. Defaults to standard error.")

logJSON = flag.Bool("log-json", false, "log in JSON format")
logLevel = flag.String("log-level", "info", "log level (options: debug, info, warn, error)")
logSource = flag.Bool("log-source", false, "log output includes source code location")
)

//go:embed favicon.ico
Expand All @@ -31,7 +39,14 @@ func init() {
}

func main() {
logger := getLogger(*errlog)
logger, err := getLogger(*errlog, *logLevel, *logSource, *logJSON)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err)
os.Exit(1)
}

slog.SetDefault(logger)

options := []hookah.ServerOption{
hookah.ServerExecTimeout(*timeout),
hookah.ServerErrorLog(logger),
Expand All @@ -43,7 +58,8 @@ func main() {

hServe, err := hookah.NewHookServer(*serverRoot, options...)
if err != nil {
log.Fatal(err)
slog.Error("failed to create hook server", "error", err)
os.Exit(1)
}

var serve http.Handler = hServe
Expand All @@ -58,21 +74,54 @@ func main() {
w.Write(favicon)
})

logger.Println("listening on port", *httpPort)
logger.Info("listening on port", "port", *httpPort)
err = http.ListenAndServe(":"+strconv.Itoa(int(*httpPort)), mux)
if err != nil {
log.Fatal(err)
slog.Error("failed to start HTTP server", "error", err)
os.Exit(1)
}
}

func getLogger(filename string) hookah.Logger {
func getLogger(filename, logLevel string, addSource, json bool) (*slog.Logger, error) {
var handler slog.Handler

var level slog.Level
switch logLevel {
case "debug":
level = slog.LevelDebug
case "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
slog.Error("unknown log level", "level", logLevel)
os.Exit(3)
Comment on lines +99 to +100
Copy link

Copilot AI Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Calling os.Exit and logging with the global logger here hinders testability and errors handling. Instead, return an error for unknown levels and let the caller decide to exit.

Suggested change
slog.Error("unknown log level", "level", logLevel)
os.Exit(3)
return nil, fmt.Errorf("unknown log level: %s", logLevel)

Copilot uses AI. Check for mistakes.
}

opts := &slog.HandlerOptions{
AddSource: addSource,
Level: level,
}

var w io.Writer
if filename != "" {
f, err := os.OpenFile(*errlog, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
return nil, fmt.Errorf("failed to open log file: %w", err)
}
return log.New(f, "", log.LstdFlags)

w = f
} else {
w = os.Stderr
}

if json {
handler = slog.NewJSONHandler(w, opts)
} else {
handler = slog.NewTextHandler(w, opts)
}

return log.New(os.Stderr, "", log.LstdFlags)
return slog.New(handler), nil
}
11 changes: 6 additions & 5 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"path/filepath"
Expand All @@ -19,7 +20,7 @@ import (
type HookExec struct {
RootDir string
Data io.ReadSeeker
InfoLog Logger
InfoLog *slog.Logger

Stdout io.Writer
Stderr io.Writer
Expand Down Expand Up @@ -65,8 +66,8 @@ func (h *HookExec) GetPathExecs(owner, repo, event, action string) ([]string, []
}

// pathScan scans the given path for executable files
// returns a list of files and a list of error handlers
// error handlers are files that start with @@error.
// returns a list of files and a list of error handlers.
// Error handlers are files that start with @@error.
func pathScan(path string) ([]string, []string, error) {
files := []string{}
errHandlers := []string{}
Expand Down Expand Up @@ -124,13 +125,13 @@ func pathScan(path string) ([]string, []string, error) {
// InfoLogf logs to the info logger if not nil
func (h *HookExec) InfoLogf(format string, v ...any) {
if h.InfoLog != nil {
h.InfoLog.Printf(format, v...)
h.InfoLog.Info(fmt.Sprintf(format, v...))
}
}

func (h *HookExec) InfoLogln(msg string) {
if h.InfoLog != nil {
h.InfoLog.Println(msg)
h.InfoLog.Info(msg)
}
}

Expand Down
6 changes: 3 additions & 3 deletions exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package hookah

import (
"bytes"
"log"
"log/slog"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -47,7 +47,7 @@ func TestOnlyExecutableBinsFound(t *testing.T) {
return
}

log.Printf("%#v", scripts)
slog.Debug("scripts", "scripts", scripts)

assert.EqualValues(t, expectedScripts, scripts)

Expand Down Expand Up @@ -98,7 +98,7 @@ func TestActionDirectoriesWorkAsExpected(t *testing.T) {
return
}

log.Printf("%#v", scripts)
slog.Debug("scripts", "scripts", scripts)

assert.EqualValues(t, expectedScripts, scripts)

Expand Down
29 changes: 14 additions & 15 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"path/filepath"
Expand All @@ -20,19 +20,13 @@ import (
var ErrPathIsNotDir = errors.New("path is not a dir")
var validGhEvent = regexp.MustCompile(`^[a-z\d_]{1,30}$`)

// Logger handles Printf
type Logger interface {
Printf(format string, v ...any)
Println(v ...any)
}

// HookServer implements net/http.Handler
type HookServer struct {
RootDir string

Timeout time.Duration
ErrorLog Logger
InfoLog Logger
ErrorLog *slog.Logger
InfoLog *slog.Logger

sync.Mutex
}
Expand Down Expand Up @@ -86,15 +80,15 @@ func ServerExecTimeout(timeout time.Duration) ServerOption {
}

// ServerErrorLog configures the HookServer error logger
func ServerErrorLog(log Logger) ServerOption {
func ServerErrorLog(log *slog.Logger) ServerOption {
return func(h *HookServer) error {
h.ErrorLog = log
return nil
}
}

// ServerInfoLog configures the HookServer info logger
func ServerInfoLog(log Logger) ServerOption {
func ServerInfoLog(log *slog.Logger) ServerOption {
return func(h *HookServer) error {
h.InfoLog = log
return nil
Expand Down Expand Up @@ -123,7 +117,7 @@ func (h *HookServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(ghDelivery, err)
slog.Info("request error", "delivery", ghDelivery, "error", err)
Copy link

Copilot AI Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses the global logger and logs at Info level—errors should use the injected h.ErrorLog at Error level (e.g. h.ErrorLog.Error(...)) to respect server configuration.

Suggested change
slog.Info("request error", "delivery", ghDelivery, "error", err)
if h.ErrorLog != nil {
h.ErrorLog.Error("request error", "delivery", ghDelivery, "error", err)
}

Copilot uses AI. Check for mistakes.
return
}
buff := bytes.NewReader(b)
Expand All @@ -134,7 +128,7 @@ func (h *HookServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err = decoder.Decode(basicHook)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Println(ghDelivery, err)
slog.Info("json decode error", "delivery", ghDelivery, "error", err)
Copy link

Copilot AI Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this global slog.Info call with the instance’s h.ErrorLog.Error(...) to log decoding failures correctly and honor configured log destinations and levels.

Suggested change
slog.Info("json decode error", "delivery", ghDelivery, "error", err)
if h.ErrorLog != nil {
h.ErrorLog.Error("json decode error",
"delivery", ghDelivery,
"error", err)
}

Copilot uses AI. Check for mistakes.
return
}

Expand All @@ -143,7 +137,7 @@ func (h *HookServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if repo == "" || login == "" {
msg := "Unexpected JSON HTTP Body"
http.Error(w, msg, http.StatusBadRequest)
log.Println(ghDelivery, msg)
slog.Info("invalid request body", "delivery", ghDelivery, "message", msg)
return
}

Expand All @@ -170,7 +164,12 @@ func (h *HookServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"HOOKAH_SERVER_ROOT="+h.RootDir,
)
if err != nil && h.ErrorLog != nil {
h.ErrorLog.Printf("%s - %s/%s:%s - '%s'", ghDelivery, login, repo, ghEvent, err)
h.ErrorLog.Error("hook execution error",
"delivery", ghDelivery,
"login", login,
"repo", repo,
"event", ghEvent,
"error", err)
}
}()
}
Expand Down
Loading