some changes
This commit is contained in:
3
Makefile
3
Makefile
@@ -7,6 +7,7 @@ BINARY = ${BIN_DIR}/$(NAME)
|
|||||||
CHECK_LINTER = command -v golangci-lint >/dev/null 2>&1
|
CHECK_LINTER = command -v golangci-lint >/dev/null 2>&1
|
||||||
CHECK_IMPORTS = command -v goimports >/dev/null 2>&1
|
CHECK_IMPORTS = command -v goimports >/dev/null 2>&1
|
||||||
PATH := $(PATH):$(HOME)/go/bin
|
PATH := $(PATH):$(HOME)/go/bin
|
||||||
|
VERSION = 0.0.1-dev
|
||||||
|
|
||||||
lint-tools:
|
lint-tools:
|
||||||
@if ! $(CHECK_LINTER); then \
|
@if ! $(CHECK_LINTER); then \
|
||||||
@@ -27,7 +28,7 @@ run: build
|
|||||||
@echo "-- running $(NAME)"
|
@echo "-- running $(NAME)"
|
||||||
@$(BINARY)
|
@$(BINARY)
|
||||||
|
|
||||||
#BUILD_PARAMS = -trimpath
|
BUILD_PARAMS = -trimpath -ldflags "-X git.oblat.lv/alex/triggerssmith/internal/vars.Version=$(VERSION)"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo "-- building $(NAME)"
|
@echo "-- building $(NAME)"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package invoke
|
package invoke
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.oblat.lv/alex/triggerssmith/internal/config"
|
"git.oblat.lv/alex/triggerssmith/internal/config"
|
||||||
@@ -22,6 +25,46 @@ type Function struct {
|
|||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
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 {
|
func InvokeHandler(cfg *config.Config) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "function_id")
|
id := chi.URLParam(r, "function_id")
|
||||||
@@ -49,8 +92,46 @@ func InvokeHandler(cfg *config.Config) http.HandlerFunc {
|
|||||||
return
|
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)
|
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 {
|
if err != nil {
|
||||||
slog.Error("Failed to run function", slog.String("err", err.Error()))
|
slog.Error("Failed to run function", slog.String("err", err.Error()))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ func (r *Router) RouteHandler() chi.Router {
|
|||||||
})
|
})
|
||||||
w.Write([]byte(b))
|
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
|
return r.r
|
||||||
}
|
}
|
||||||
|
|||||||
58
cmd/serve.go
58
cmd/serve.go
@@ -4,11 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.oblat.lv/alex/triggerssmith/api"
|
"git.oblat.lv/alex/triggerssmith/api"
|
||||||
application "git.oblat.lv/alex/triggerssmith/internal/app"
|
application "git.oblat.lv/alex/triggerssmith/internal/app"
|
||||||
@@ -21,30 +20,36 @@ import (
|
|||||||
var optsServeCmd = struct {
|
var optsServeCmd = struct {
|
||||||
ConfigPath *string
|
ConfigPath *string
|
||||||
Debug *bool
|
Debug *bool
|
||||||
|
HideGreetings *bool
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
// simple middleware for request logging
|
// // simple middleware for request logging
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
// func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
// start := time.Now()
|
||||||
|
|
||||||
slog.Info("HTTP request",
|
// slog.Info("HTTP request",
|
||||||
slog.String("method", r.Method),
|
// slog.String("method", r.Method),
|
||||||
slog.String("path", r.URL.Path),
|
// slog.String("path", r.URL.Path),
|
||||||
slog.String("remote", r.RemoteAddr),
|
// slog.String("remote", r.RemoteAddr),
|
||||||
)
|
// )
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
// next.ServeHTTP(w, r)
|
||||||
|
|
||||||
slog.Debug("HTTP request finished",
|
// slog.Debug("HTTP request finished",
|
||||||
slog.String("method", r.Method),
|
// slog.String("method", r.Method),
|
||||||
slog.String("path", r.URL.Path),
|
// slog.String("path", r.URL.Path),
|
||||||
slog.Duration("latency", time.Since(start)),
|
// slog.Duration("latency", time.Since(start)),
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
func writePID(path string) error {
|
func writePID(path string) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
err := os.MkdirAll(dir, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
pid := os.Getpid()
|
pid := os.Getpid()
|
||||||
|
|
||||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
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",
|
Use: "serve",
|
||||||
Short: "Start the server",
|
Short: "Start the server",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
text := fmt.Sprintf(`
|
||||||
|
_______ _____
|
||||||
|
|__ __/ ____|
|
||||||
|
| | | (___
|
||||||
|
| | \___ \
|
||||||
|
| | ____) |
|
||||||
|
|_| |_____/
|
||||||
|
|
||||||
|
TriggerSmith - v%s
|
||||||
|
`, vars.Version)
|
||||||
|
if !*optsServeCmd.HideGreetings {
|
||||||
|
fmt.Println(text)
|
||||||
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
slog.Error("Application panicked", slog.Any("error", r))
|
slog.Error("Application panicked", slog.Any("error", r))
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// configure logger
|
// configure logger
|
||||||
if *optsServeCmd.Debug {
|
if *optsServeCmd.Debug {
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})))
|
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() {
|
func init() {
|
||||||
optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs")
|
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.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)
|
rootCmd.AddCommand(serveCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"driver": "sqlite",
|
"driver": "sqlite",
|
||||||
"path": "data.sqlite3"
|
"path": "data.sqlite3"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"log_root_path": "log"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,8 @@
|
|||||||
"name": "echo",
|
"name": "echo",
|
||||||
"version": "0.0.1-00130112025",
|
"version": "0.0.1-00130112025",
|
||||||
"entry": "echo.sh",
|
"entry": "echo.sh",
|
||||||
"runtime": "exec"
|
"runtime": "exec",
|
||||||
|
"log": {
|
||||||
|
"output": "stdout"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
read text
|
urldecode() {
|
||||||
echo "${text}"
|
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)
|
||||||
3
internal/vars/version.go
Normal file
3
internal/vars/version.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package vars
|
||||||
|
|
||||||
|
var Version = "0.0.0-none"
|
||||||
@@ -10,11 +10,18 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Write(line string)
|
||||||
|
}
|
||||||
|
|
||||||
type RootConfig struct {
|
type RootConfig struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
Driver string `json:"driver"`
|
Driver string `json:"driver"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
|
Log struct {
|
||||||
|
Path string `json:"log_root_path"`
|
||||||
|
} `json:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Function struct {
|
type Function struct {
|
||||||
@@ -30,6 +37,9 @@ type FuncConfig struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
Runtime string `json:"runtime"`
|
Runtime string `json:"runtime"`
|
||||||
|
Log struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
} `json:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadTreeConfig(root string) (*RootConfig, error) {
|
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) {
|
func FindFunction(db *gorm.DB, name, version string) (*Function, error) {
|
||||||
var f Function
|
var f Function
|
||||||
if err := db.Where("function_name = ? AND version = ? AND deleted_at IS NULL", name, version).
|
if version == "latest" {
|
||||||
First(&f).Error; err != nil {
|
err := db.Where("function_name = ? AND deleted_at IS NULL", name).
|
||||||
return nil, err
|
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
|
return &f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,49 @@
|
|||||||
package worker
|
package worker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunFunction(path string, fc *FuncConfig, input []byte) ([]byte, error) {
|
type RunOps struct {
|
||||||
if fc.Runtime != "exec" {
|
Log Logger
|
||||||
return nil, fmt.Errorf("unsupported runtime: %s", fc.Runtime)
|
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)
|
cmd.Stdin = bytes.NewReader(input)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
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 nil, fmt.Errorf("failed: %w\noutput: %s", err, out.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return out.Bytes(), nil
|
return out.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
2
log/echo:1d965976/event.log.json
Normal file
2
log/echo:1d965976/event.log.json
Normal file
@@ -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"}
|
||||||
Reference in New Issue
Block a user