diff --git a/Makefile b/Makefile index 10b8c7e..7406fc8 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ BINARY = ${BIN_DIR}/$(NAME) CHECK_LINTER = command -v golangci-lint >/dev/null 2>&1 CHECK_IMPORTS = command -v goimports >/dev/null 2>&1 PATH := $(PATH):$(HOME)/go/bin +VERSION = 0.0.1-dev lint-tools: @if ! $(CHECK_LINTER); then \ @@ -27,7 +28,7 @@ run: build @echo "-- running $(NAME)" @$(BINARY) -#BUILD_PARAMS = -trimpath +BUILD_PARAMS = -trimpath -ldflags "-X git.oblat.lv/alex/triggerssmith/internal/vars.Version=$(VERSION)" build: @echo "-- building $(NAME)" diff --git a/api/invoke/worker.go b/api/invoke/worker.go index 61b4f9d..f993bd0 100644 --- a/api/invoke/worker.go +++ b/api/invoke/worker.go @@ -1,10 +1,13 @@ package invoke import ( + "fmt" "io" "log/slog" "net/http" + "os" "path/filepath" + "strings" "time" "git.oblat.lv/alex/triggerssmith/internal/config" @@ -22,6 +25,46 @@ type Function struct { DeletedAt gorm.DeletedAt `gorm:"index"` } +type TerminalLogger struct { + fc *worker.FuncConfig +} + +func (l *TerminalLogger) Write(line string) { + slog.Warn("function stderr", slog.String("line", line), slog.String("n:v", fmt.Sprintf("%s:%s", l.fc.Name, l.fc.Version))) +} + +type JSONFileLogger struct { + fc *worker.FuncConfig + logger *slog.Logger +} + +func NewJSONFileLogger(fc *worker.FuncConfig, path string) (*JSONFileLogger, error) { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + + handler := slog.NewJSONHandler(file, &slog.HandlerOptions{}) + logger := slog.New(handler) + + return &JSONFileLogger{ + fc: fc, + logger: logger, + }, nil +} + +func (l *JSONFileLogger) Write(line string) { + l.logger.Warn("function stderr", + slog.String("function", l.fc.Name), + slog.String("version", l.fc.Version), + slog.String("line", line), + ) +} + func InvokeHandler(cfg *config.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "function_id") @@ -49,8 +92,46 @@ func InvokeHandler(cfg *config.Config) http.HandlerFunc { return } + var logger worker.Logger + switch fc.Log.Output { + case "stdout": + logger = &TerminalLogger{ + fc: fc, + } + case "file": + fileLogger, err := NewJSONFileLogger(fc, filepath.Join(treeCfg.Log.Path, fmt.Sprintf("%s:%s", fc.Name, f.Path), "event.log.json")) + if err != nil { + slog.Error("Failed to create file logger", slog.String("err", err.Error())) + w.WriteHeader(http.StatusInternalServerError) + return + } + logger = fileLogger + } + + var frmt = func(s1 string, s2 string) string { + return fmt.Sprintf("FAAS_%s=%s", s1, s2) + } + var env = []string{ + frmt("PROTOCOL", r.Proto), + + frmt("METHOD", r.Method), + frmt("PATH", r.URL.Path), + frmt("QUERY", r.URL.RawQuery), + } + + for k, v := range r.Header { + key := "FAAS_HEADER_" + strings.ReplaceAll(k, "-", "_") + env = append(env, key+"="+v[0]) + } + input, _ := io.ReadAll(r.Body) - output, err := worker.RunFunction(filepath.Join(root, f.FunctionName, f.Path, fc.Entry), fc, input) + path := filepath.Join(root, f.FunctionName, f.Path, fc.Entry) + output, err := worker.RunFunction(&worker.RunOps{ + Path: path, + FuncConfig: fc, + Log: logger, + Env: env, + }, input) if err != nil { slog.Error("Failed to run function", slog.String("err", err.Error())) w.WriteHeader(http.StatusInternalServerError) diff --git a/api/router.go b/api/router.go index 1993d40..6acc98a 100644 --- a/api/router.go +++ b/api/router.go @@ -42,6 +42,6 @@ func (r *Router) RouteHandler() chi.Router { }) w.Write([]byte(b)) }) - r.r.Handle("/i/invoke/function/{function_id}/{function_version}", invoke.InvokeHandler(r.cfg)) + r.r.Handle("/invoke/function/{function_id}/{function_version}", invoke.InvokeHandler(r.cfg)) return r.r } diff --git a/cmd/serve.go b/cmd/serve.go index 7467230..cbef70c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -4,11 +4,10 @@ import ( "fmt" "log/slog" "net" - "net/http" "os" "os/signal" + "path/filepath" "syscall" - "time" "git.oblat.lv/alex/triggerssmith/api" application "git.oblat.lv/alex/triggerssmith/internal/app" @@ -21,30 +20,36 @@ import ( var optsServeCmd = struct { ConfigPath *string Debug *bool + HideGreetings *bool }{} -// simple middleware for request logging -func loggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() +// // simple middleware for request logging +// func loggingMiddleware(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// start := time.Now() - slog.Info("HTTP request", - slog.String("method", r.Method), - slog.String("path", r.URL.Path), - slog.String("remote", r.RemoteAddr), - ) +// slog.Info("HTTP request", +// slog.String("method", r.Method), +// slog.String("path", r.URL.Path), +// slog.String("remote", r.RemoteAddr), +// ) - next.ServeHTTP(w, r) +// next.ServeHTTP(w, r) - slog.Debug("HTTP request finished", - slog.String("method", r.Method), - slog.String("path", r.URL.Path), - slog.Duration("latency", time.Since(start)), - ) - }) -} +// slog.Debug("HTTP request finished", +// slog.String("method", r.Method), +// slog.String("path", r.URL.Path), +// slog.Duration("latency", time.Since(start)), +// ) +// }) +// } func writePID(path string) error { + dir := filepath.Dir(path) + err := os.MkdirAll(dir, 0644) + if err != nil { + return nil + } pid := os.Getpid() f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) @@ -61,12 +66,26 @@ var serveCmd = &cobra.Command{ Use: "serve", Short: "Start the server", Run: func(cmd *cobra.Command, args []string) { + text := fmt.Sprintf(` + _______ _____ + |__ __/ ____| + | | | (___ + | | \___ \ + | | ____) | + |_| |_____/ + + TriggerSmith - v%s + `, vars.Version) + if !*optsServeCmd.HideGreetings { + fmt.Println(text) + } defer func() { if r := recover(); r != nil { slog.Error("Application panicked", slog.Any("error", r)) os.Exit(-1) } }() + // configure logger if *optsServeCmd.Debug { slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))) @@ -170,5 +189,6 @@ var serveCmd = &cobra.Command{ func init() { optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") + optsServeCmd.HideGreetings = serveCmd.Flags().BoolP("hide-greetings", "g", false, "Hide the welcome message and version when starting the server") rootCmd.AddCommand(serveCmd) } diff --git a/functions/config.json b/functions/config.json index 2f8b5e7..53c7c59 100644 --- a/functions/config.json +++ b/functions/config.json @@ -2,5 +2,8 @@ "data": { "driver": "sqlite", "path": "data.sqlite3" + }, + "log": { + "log_root_path": "log" } } \ No newline at end of file diff --git a/functions/echo/1d965976/config.json b/functions/echo/1d965976/config.json index a7d215e..4418420 100644 --- a/functions/echo/1d965976/config.json +++ b/functions/echo/1d965976/config.json @@ -2,5 +2,8 @@ "name": "echo", "version": "0.0.1-00130112025", "entry": "echo.sh", - "runtime": "exec" + "runtime": "exec", + "log": { + "output": "stdout" + } } \ No newline at end of file diff --git a/functions/echo/1d965976/echo.sh b/functions/echo/1d965976/echo.sh index 2c05063..bf10dc8 100755 --- a/functions/echo/1d965976/echo.sh +++ b/functions/echo/1d965976/echo.sh @@ -1,4 +1,21 @@ #!/bin/bash -read text -echo "${text}" \ No newline at end of file +urldecode() { + local data="${1//+/ }" + printf '%b' "${data//%/\\x}" +} + +declare -A QUERY + +IFS='&' read -ra pairs <<< "$FAAS_QUERY" + +for pair in "${pairs[@]}"; do + IFS='=' read -r raw_key raw_value <<< "$pair" + key=$(urldecode "$raw_key") + value=$(urldecode "$raw_value") + QUERY["$key"]="$value" +done + +echo "a = ${QUERY[a]}" +echo "b = ${QUERY[b]}" +#echo $(ls) \ No newline at end of file diff --git a/internal/vars/version.go b/internal/vars/version.go new file mode 100644 index 0000000..0e0fd59 --- /dev/null +++ b/internal/vars/version.go @@ -0,0 +1,3 @@ +package vars + +var Version = "0.0.0-none" diff --git a/internal/worker/handle.go b/internal/worker/handle.go index 096ec2a..4e64531 100644 --- a/internal/worker/handle.go +++ b/internal/worker/handle.go @@ -10,11 +10,18 @@ import ( "gorm.io/gorm" ) +type Logger interface { + Write(line string) +} + type RootConfig struct { Data struct { Driver string `json:"driver"` Path string `json:"path"` } `json:"data"` + Log struct { + Path string `json:"log_root_path"` + } `json:"log"` } type Function struct { @@ -30,6 +37,9 @@ type FuncConfig struct { Version string `json:"version"` Entry string `json:"entry"` Runtime string `json:"runtime"` + Log struct { + Output string `json:"output"` + } `json:"log"` } func LoadTreeConfig(root string) (*RootConfig, error) { @@ -61,9 +71,18 @@ func OpenDB(cfg *RootConfig, root string) (*gorm.DB, error) { func FindFunction(db *gorm.DB, name, version string) (*Function, error) { var f Function - if err := db.Where("function_name = ? AND version = ? AND deleted_at IS NULL", name, version). - First(&f).Error; err != nil { - return nil, err + if version == "latest" { + err := db.Where("function_name = ? AND deleted_at IS NULL", name). + Order("created_at DESC"). + First(&f).Error + if err != nil { + return nil, err + } + } else { + if err := db.Where("function_name = ? AND version = ? AND deleted_at IS NULL", name, version). + First(&f).Error; err != nil { + return nil, err + } } return &f, nil } diff --git a/internal/worker/run.go b/internal/worker/run.go index a40bb75..8ee6063 100644 --- a/internal/worker/run.go +++ b/internal/worker/run.go @@ -1,25 +1,49 @@ package worker import ( + "bufio" "bytes" "fmt" "os/exec" ) -func RunFunction(path string, fc *FuncConfig, input []byte) ([]byte, error) { - if fc.Runtime != "exec" { - return nil, fmt.Errorf("unsupported runtime: %s", fc.Runtime) +type RunOps struct { + Log Logger + Path string + FuncConfig *FuncConfig + Env []string +} + +func RunFunction(opt *RunOps, input []byte) ([]byte, error) { + if opt.FuncConfig.Runtime != "exec" { + return nil, fmt.Errorf("unsupported runtime: %s", opt.FuncConfig.Runtime) } - cmd := exec.Command(path) + cmd := exec.Command(opt.Path) + cmd.Env = opt.Env cmd.Stdin = bytes.NewReader(input) var out bytes.Buffer cmd.Stdout = &out - cmd.Stderr = &out + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + go func() { + scanner := bufio.NewScanner(stderrPipe) + for scanner.Scan() { + line := scanner.Text() + opt.Log.Write(line) + } + }() - if err := cmd.Run(); err != nil { + if err := cmd.Start(); err != nil { + return nil, err + } + + if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("failed: %w\noutput: %s", err, out.String()) } + return out.Bytes(), nil } diff --git a/log/echo:1d965976/event.log.json b/log/echo:1d965976/event.log.json new file mode 100644 index 0000000..c8dc807 --- /dev/null +++ b/log/echo:1d965976/event.log.json @@ -0,0 +1,2 @@ +{"time":"2025-11-30T16:12:41.425709604+02:00","level":"WARN","msg":"function stderr","function":"echo","version":"0.0.1-00130112025","line":"bem bem"} +{"time":"2025-11-30T16:12:42.487539993+02:00","level":"WARN","msg":"function stderr","function":"echo","version":"0.0.1-00130112025","line":"bem bem"}