Refactor error handling and utility functions; remove deprecated code and improve logging

This commit is contained in:
alex
2025-07-05 16:05:03 +03:00
parent b70819e976
commit 2fdc32ce9f
13 changed files with 132 additions and 162 deletions

View File

@@ -63,7 +63,6 @@ func main() {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
}) })
}) })
r.NotFound(serverv1.ErrNotFound)
address := cfg.Address address := cfg.Address
if cfg.TlsEnabled { if cfg.TlsEnabled {

View File

@@ -1,16 +0,0 @@
if not Params.f then
Result.status = "error"
Result.error = "Missing parameter: f"
return
end
local code = os.execute("touch " .. Params.f)
if code ~= 0 then
Result.status = "error"
Result.message = "Failed to execute command"
return
end
Result.status = "ok"
Result.message = "Command executed successfully"

View File

@@ -5,13 +5,15 @@ var UUIDLength byte = 4
// ApiRoute setting for go-chi for main route for api requests // ApiRoute setting for go-chi for main route for api requests
var ApiRoute string = "/api/{ver}" var ApiRoute string = "/api/{ver}"
// ComDirRoute setting for go-chi for main route for commands // ComDirRoute setting for go-chi for main route for commands
var ComDirRoute string = "/com" var ComDirRoute string = "/com"
// NodeVersion is the version of the node. It can be set by the build system or manually. // NodeVersion is the version of the node. It can be set by the build system or manually.
// If not set, it will return "version0.0.0-none" by default // If not set, it will return "version0.0.0-none" by default
var NodeVersion string var NodeVersion string
// ActualFileName is a feature of the GoSally update system.
// ActualFileName is a feature of the GoSally update system.
// In the repository, the file specified in the variable contains the current information about updates // In the repository, the file specified in the variable contains the current information about updates
var ActualFileName string = "actual.txt" var ActualFileName string = "actual.txt"
@@ -28,7 +30,7 @@ func (_ _updateConsts) GetNodeVersion() string {
} }
func (_ _updateConsts) GetActualFileName() string { return ActualFileName } func (_ _updateConsts) GetActualFileName() string { return ActualFileName }
func GetInternalConsts() _internalConsts { return _internalConsts{} } func GetInternalConsts() _internalConsts { return _internalConsts{} }
func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength } func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength }
func GetServerConsts() _serverConsts { return _serverConsts{} } func GetServerConsts() _serverConsts { return _serverConsts{} }

View File

@@ -14,13 +14,13 @@
package general_server package general_server
import ( import (
"encoding/json"
"errors" "errors"
"log/slog" "log/slog"
"net/http" "net/http"
"slices" "slices"
"github.com/akyaiy/GoSally-mvp/core/config" "github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -140,7 +140,7 @@ func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) {
log.Error("HTTP request error: unsupported API version", log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest)) slog.Int("status", http.StatusBadRequest))
s.writeJSONError(http.StatusBadRequest, "unsupported API version") utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version")
} }
// HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the API version. // HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the API version.
@@ -182,23 +182,5 @@ func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) {
log.Error("HTTP request error: unsupported API version", log.Error("HTTP request error: unsupported API version",
slog.Int("status", http.StatusBadRequest)) slog.Int("status", http.StatusBadRequest))
s.writeJSONError(http.StatusBadRequest, "unsupported API version") utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version")
}
// writeJSONError writes a JSON error response to the HTTP response writer.
// It sets the Content-Type to application/json, writes the specified HTTP status code,
func (s *GeneralServer) writeJSONError(status int, msg string) {
s.w.Header().Set("Content-Type", "application/json")
s.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(s.w).Encode(resp); err != nil {
s.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()),
slog.Int("status", status))
return
}
} }

View File

@@ -1,3 +1,6 @@
// Package logs provides a logger setup function that configures the logger based on the environment.
// It supports different logging levels for development and production environments.
// It uses the standard library's slog package for structured logging.
package logs package logs
import ( import (
@@ -5,11 +8,15 @@ import (
"os" "os"
) )
// Environment constants for logger setup
const ( const (
envDev = "dev" // envDev enables development logging with debug level
envDev = "dev"
// envProd enables production logging with info level
envProd = "prod" envProd = "prod"
) )
// SetupLogger initializes and returns a logger based on the provided environment.
func SetupLogger(env string) *slog.Logger { func SetupLogger(env string) *slog.Logger {
var log *slog.Logger var log *slog.Logger
switch env { switch env {

View File

@@ -6,30 +6,20 @@ import (
"sync" "sync"
) )
// MockHandler is a mock implementation of slog.Handler for testing purposes.
type MockHandler struct { type MockHandler struct {
mu sync.Mutex mu sync.Mutex
// Logs stores the log records captured by the handler.
Logs []slog.Record Logs []slog.Record
} }
func NewMockHandler() *MockHandler { func NewMockHandler() *MockHandler { return &MockHandler{} }
return &MockHandler{} func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
} func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
func (h *MockHandler) WithGroup(_ string) slog.Handler { return h }
func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
func (h *MockHandler) Handle(_ context.Context, r slog.Record) error { func (h *MockHandler) Handle(_ context.Context, r slog.Record) error {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
h.Logs = append(h.Logs, r.Clone()) h.Logs = append(h.Logs, r.Clone())
return nil return nil
} }
func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h
}
func (h *MockHandler) WithGroup(_ string) slog.Handler {
return h
}

View File

@@ -7,12 +7,22 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
func (h *HandlerV1) _handle() { // HandlerV1 is the main handler for version 1 of the API.
uuid16 := h.newUUID() // The function processes the HTTP request and runs Lua scripts,
// preparing the environment and subsequently transmitting the execution result
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID()
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
return
}
log := h.log.With( log := h.log.With(
slog.Group("request", slog.Group("request",
slog.String("version", h.GetVersion()), slog.String("version", h.GetVersion()),
@@ -32,7 +42,7 @@ func (h *HandlerV1) _handle() {
slog.String("error", "invalid command"), slog.String("error", "invalid command"),
slog.String("cmd", cmd), slog.String("cmd", cmd),
slog.Int("status", http.StatusBadRequest)) slog.Int("status", http.StatusBadRequest))
h.writeJSONError(http.StatusBadRequest, "invalid command") utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid command")
return return
} }
@@ -42,7 +52,7 @@ func (h *HandlerV1) _handle() {
slog.String("error", "command not found"), slog.String("error", "command not found"),
slog.String("cmd", cmd), slog.String("cmd", cmd),
slog.Int("status", http.StatusNotFound)) slog.Int("status", http.StatusNotFound))
h.writeJSONError(http.StatusNotFound, "command not found") utils.WriteJSONError(h.w, http.StatusNotFound, "command not found")
return return
} }
@@ -52,15 +62,13 @@ func (h *HandlerV1) _handle() {
slog.String("error", "command not found"), slog.String("error", "command not found"),
slog.String("cmd", cmd), slog.String("cmd", cmd),
slog.Int("status", http.StatusNotFound)) slog.Int("status", http.StatusNotFound))
h.writeJSONError(http.StatusNotFound, "command not found") utils.WriteJSONError(h.w, http.StatusNotFound, "command not found")
return return
} }
L := lua.NewState() L := lua.NewState()
defer L.Close() defer L.Close()
// Создаем таблицу Params
// Создаем таблицу In с Params
paramsTable := L.NewTable() paramsTable := L.NewTable()
qt := h.r.URL.Query() qt := h.r.URL.Query()
for k, v := range qt { for k, v := range qt {
@@ -78,49 +86,44 @@ func (h *HandlerV1) _handle() {
L.SetField(outTable, "Result", resultTable) L.SetField(outTable, "Result", resultTable)
L.SetGlobal("Out", outTable) L.SetGlobal("Out", outTable)
// Скрипт подготовки окружения
prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua") prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua")
if _, err := os.Stat(prepareLuaEnv); err == nil { if _, err := os.Stat(prepareLuaEnv); err == nil {
if err := L.DoFile(prepareLuaEnv); err != nil { if err := L.DoFile(prepareLuaEnv); err != nil {
log.Error("Failed to prepare lua environment", log.Error("Failed to prepare lua environment",
slog.String("error", err.Error())) slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error())
return return
} }
} else { } else {
log.Warn("No environment preparation script found, skipping preparation") log.Warn("No environment preparation script found, skipping preparation")
} }
// Основной Lua скрипт
if err := L.DoFile(scriptPath); err != nil { if err := L.DoFile(scriptPath); err != nil {
log.Error("Failed to execute lua script", log.Error("Failed to execute lua script",
slog.String("error", err.Error())) slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error())
return return
} }
// Получаем Out
lv := L.GetGlobal("Out") lv := L.GetGlobal("Out")
tbl, ok := lv.(*lua.LTable) tbl, ok := lv.(*lua.LTable)
if !ok { if !ok {
log.Error("Lua global 'Out' is not a table") log.Error("Lua global 'Out' is not a table")
h.writeJSONError(http.StatusInternalServerError, "'Out' is not a table") utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Out' is not a table")
return return
} }
// Получаем Result из Out
resultVal := tbl.RawGetString("Result") resultVal := tbl.RawGetString("Result")
resultTbl, ok := resultVal.(*lua.LTable) resultTbl, ok := resultVal.(*lua.LTable)
if !ok { if !ok {
log.Error("Lua global 'Result' is not a table") log.Error("Lua global 'Result' is not a table")
h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table") utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Result' is not a table")
return return
} }
// Перебираем таблицу Result
out := make(map[string]interface{}) out := make(map[string]interface{})
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) { resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
out[key.String()] = convertTypes(value) out[key.String()] = utils.ConvertLuaTypesToGolang(value)
}) })
h.w.Header().Set("Content-Type", "application/json") h.w.Header().Set("Content-Type", "application/json")
@@ -147,26 +150,3 @@ func (h *HandlerV1) _handle() {
log.Info("Session completed") log.Info("Session completed")
} }
func convertTypes(value lua.LValue) any {
switch value.Type() {
case lua.LTString:
return value.String()
case lua.LTNumber:
return float64(value.(lua.LNumber))
case lua.LTBool:
return bool(value.(lua.LBool))
case lua.LTTable:
result := make(map[string]interface{})
if tbl, ok := value.(*lua.LTable); ok {
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
result[key.String()] = convertTypes(value)
})
}
return result
case lua.LTNil:
return nil
default:
return value.String()
}
}

View File

@@ -8,11 +8,19 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
func (h *HandlerV1) _handleList() { // The function processes the HTTP request and returns a list of available commands.
uuid16 := h.newUUID() func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID()
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error())
return
}
log := h.log.With( log := h.log.With(
slog.Group("request", slog.Group("request",
slog.String("version", h.GetVersion()), slog.String("version", h.GetVersion()),
@@ -31,7 +39,6 @@ func (h *HandlerV1) _handleList() {
} }
var ( var (
files []os.DirEntry files []os.DirEntry
err error
commands = make(map[string]ComMeta) commands = make(map[string]ComMeta)
cmdsProcessed = make(map[string]bool) cmdsProcessed = make(map[string]bool)
) )
@@ -39,7 +46,7 @@ func (h *HandlerV1) _handleList() {
if files, err = os.ReadDir(h.cfg.ComDir); err != nil { if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
log.Error("Failed to read commands directory", log.Error("Failed to read commands directory",
slog.String("error", err.Error())) slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
return return
} }

View File

@@ -1,3 +1,5 @@
// Package sv1 provides the implementation of the Server V1 API handler.
// It includes utilities for handling API requests, extracting descriptions, and managing UUIDs.
package sv1 package sv1
import ( import (
@@ -8,16 +10,7 @@ import (
"github.com/akyaiy/GoSally-mvp/core/config" "github.com/akyaiy/GoSally-mvp/core/config"
) )
type ServerV1UtilsContract interface { // HandlerV1InitStruct structure is only for initialization
extractDescriptionStatic(path string) (string, error)
writeJSONError(status int, msg string)
newUUID() string
_errNotFound()
ErrNotFound(w http.ResponseWriter, r *http.Request)
}
// structure only for initialization
type HandlerV1InitStruct struct { type HandlerV1InitStruct struct {
Ver string Ver string
Log slog.Logger Log slog.Logger
@@ -26,6 +19,7 @@ type HandlerV1InitStruct struct {
ListAllowedCmd *regexp.Regexp ListAllowedCmd *regexp.Regexp
} }
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
type HandlerV1 struct { type HandlerV1 struct {
w http.ResponseWriter w http.ResponseWriter
r *http.Request r *http.Request
@@ -34,12 +28,16 @@ type HandlerV1 struct {
cfg *config.ConfigConf cfg *config.ConfigConf
// allowedCmd and listAllowedCmd are regular expressions used to validate command names.
allowedCmd *regexp.Regexp allowedCmd *regexp.Regexp
listAllowedCmd *regexp.Regexp listAllowedCmd *regexp.Regexp
ver string ver string
} }
// InitV1Server initializes a new HandlerV1 with the provided configuration and returns it.
// Should be carefull with giving to this function invalid parameters,
// because there is no validation of parameters in this function.
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
return &HandlerV1{ return &HandlerV1{
log: o.Log, log: o.Log,
@@ -50,18 +48,8 @@ func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
} }
} }
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { // GetVersion returns the API version of the HandlerV1, which is set during initialization.
h.w = w // This version is used to identify the API version in the request routing.
h.r = r
h._handle()
}
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._handleList()
}
func (h *HandlerV1) GetVersion() string { func (h *HandlerV1) GetVersion() string {
return h.ver return h.ver
} }

View File

@@ -1,35 +1,16 @@
package sv1 package sv1
import ( import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"github.com/akyaiy/GoSally-mvp/core/config" "github.com/akyaiy/GoSally-mvp/core/utils"
) )
func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) { func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
h.w = w utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
h.r = r
h._errNotFound()
}
func (h *HandlerV1) newUUID() string {
bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2))
_, err := rand.Read(bytes)
if err != nil {
h.log.Error("Failed to generate UUID", slog.String("error", err.Error()))
return ""
}
return hex.EncodeToString(bytes)
}
func (h *HandlerV1) _errNotFound() {
h.writeJSONError(http.StatusBadRequest, "invalid request")
h.log.Error("HTTP request error", h.log.Error("HTTP request error",
slog.String("remote", h.r.RemoteAddr), slog.String("remote", h.r.RemoteAddr),
slog.String("method", h.r.Method), slog.String("method", h.r.Method),
@@ -37,22 +18,6 @@ func (h *HandlerV1) _errNotFound() {
slog.Int("status", http.StatusBadRequest)) slog.Int("status", http.StatusBadRequest))
} }
func (h *HandlerV1) writeJSONError(status int, msg string) {
h.w.Header().Set("Content-Type", "application/json")
h.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(h.w).Encode(resp); err != nil {
h.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()),
slog.Int("status", status))
return
}
}
func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) { func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {

22
core/utils/http_errors.go Normal file
View File

@@ -0,0 +1,22 @@
package utils
import (
"encoding/json"
"net/http"
)
// writeJSONError writes a JSON error response to the HTTP response writer.
// It sets the Content-Type to application/json, writes the specified HTTP status code
func WriteJSONError(w http.ResponseWriter, status int, msg string) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]any{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,26 @@
package utils
import lua "github.com/yuin/gopher-lua"
func ConvertLuaTypesToGolang(value lua.LValue) any {
switch value.Type() {
case lua.LTString:
return value.String()
case lua.LTNumber:
return float64(value.(lua.LNumber))
case lua.LTBool:
return bool(value.(lua.LBool))
case lua.LTTable:
result := make(map[string]interface{})
if tbl, ok := value.(*lua.LTable); ok {
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
result[key.String()] = ConvertLuaTypesToGolang(value)
})
}
return result
case lua.LTNil:
return nil
default:
return value.String()
}
}

18
core/utils/uuid.go Normal file
View File

@@ -0,0 +1,18 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"errors"
"github.com/akyaiy/GoSally-mvp/core/config"
)
func NewUUID() (string, error) {
bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2))
_, err := rand.Read(bytes)
if err != nil {
return "", errors.New("failed to generate UUID: " + err.Error())
}
return hex.EncodeToString(bytes), nil
}