diff --git a/cmd/server/main.go b/cmd/server/main.go index 9edab98..cf0eece 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -35,10 +35,15 @@ func main() { os.Exit(1) } - s := logranger.New(c) + s, err := logranger.New(c) + if err != nil { + l.Error("failed to create new server", LogErrKey, err) + os.Exit(1) + } + go func() { if err = s.Run(); err != nil { - l.Error("failed to start logranger: %s", LogErrKey, err) + l.Error("failed to start logranger", LogErrKey, err) os.Exit(1) } }() diff --git a/connection.go b/connection.go index c0b32a5..9e4bcc4 100644 --- a/connection.go +++ b/connection.go @@ -6,6 +6,8 @@ package logranger import ( "bufio" + "fmt" + "math/rand" "net" ) @@ -23,9 +25,15 @@ type Connection struct { func NewConnection(nc net.Conn) *Connection { c := &Connection{ conn: nc, - id: "foo", + id: NewConnectionID(), rb: bufio.NewReader(nc), wb: bufio.NewWriter(nc), } return c } + +// NewConnectionID generates a new unique message ID using a random number generator +// and returns it as a hexadecimal string. +func NewConnectionID() string { + return fmt.Sprintf("%x", rand.Int63()) +} diff --git a/plugins/action.go b/plugins/action.go new file mode 100644 index 0000000..8de065d --- /dev/null +++ b/plugins/action.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package plugins + +import ( + "github.com/wneessen/go-parsesyslog" +) + +// Action is an interface that defines the behavior of an action to be performed +// on a log message. +// +// The Process method takes a log message, a slice of match groups, and a +// configuration map, and returns an error if any occurs during processing. +type Action interface { + Process(logmessage parsesyslog.LogMsg, matchgroup []string, confmap map[string]any) error +} diff --git a/plugins/actions/all/all.go b/plugins/actions/all/all.go new file mode 100644 index 0000000..3333e3e --- /dev/null +++ b/plugins/actions/all/all.go @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package all diff --git a/plugins/actions/all/file.go b/plugins/actions/all/file.go new file mode 100644 index 0000000..90acd3f --- /dev/null +++ b/plugins/actions/all/file.go @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package all + +import _ "github.com/wneessen/logranger/plugins/actions/file" // register plugin diff --git a/plugins/actions/file/file.go b/plugins/actions/file/file.go new file mode 100644 index 0000000..32f1c60 --- /dev/null +++ b/plugins/actions/file/file.go @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package file + +import ( + "fmt" + "os" + + "github.com/wneessen/go-parsesyslog" + + "github.com/wneessen/logranger/plugins/actions" + "github.com/wneessen/logranger/template" +) + +// File represents a file action that can be performed on a log message. +type File struct{} + +// Process satisfies the plugins.Action interface for the File type +// It takes in the log message (lm), match groups (mg), and configuration map (cm). +func (f *File) Process(lm parsesyslog.LogMsg, mg []string, cm map[string]any) error { + if cm["file"] == nil { + return nil + } + c, ok := cm["file"].(map[string]any) + if !ok { + return fmt.Errorf("missing configuration for file action") + } + ot, ok := c["output_template"].(string) + if !ok || ot == "" { + return fmt.Errorf("not output_template configured for file action") + } + + fn, ok := c["output_filepath"].(string) + if !ok || fn == "" { + return fmt.Errorf("no output_filename configured for file action") + } + + of := os.O_APPEND | os.O_CREATE | os.O_WRONLY + if ow, ok := c["overwrite"].(bool); ok && ow { + of = os.O_WRONLY | os.O_CREATE + } + + fh, err := os.OpenFile(fn, of, 0o600) + if err != nil { + return fmt.Errorf("failed to open file for writing in file action: %w", err) + } + defer func() { + _ = fh.Close() + }() + + t, err := template.Compile(lm, mg, ot) + if err != nil { + return err + } + _, err = fh.WriteString(t) + if err != nil { + return fmt.Errorf("failed to write log message to file %q: %w", fn, err) + } + if err := fh.Sync(); err != nil { + return fmt.Errorf("failed to sync memory to file %q: %w", fn, err) + } + + return nil +} + +// init registers the "file" action with the Actions map. +func init() { + actions.Add("file", &File{}) +} diff --git a/plugins/actions/registry.go b/plugins/actions/registry.go new file mode 100644 index 0000000..a5b3f7f --- /dev/null +++ b/plugins/actions/registry.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package actions + +import ( + "github.com/wneessen/logranger/plugins" +) + +// Actions is a variable that represents a map of string keys to Action values. The keys are used to identify different actions, and the corresponding values are the functions that define +var Actions = map[string]plugins.Action{} + +// Add adds an action with the given name to the Actions map. The action function must implement the Action interface. +func Add(name string, action plugins.Action) { + Actions[name] = action +} diff --git a/rule.go b/rule.go index e70fbce..e3fc99b 100644 --- a/rule.go +++ b/rule.go @@ -14,12 +14,17 @@ import ( "github.com/kkyr/fig" ) +// Ruleset represents a collection of rules. type Ruleset struct { - Rule []struct { - ID string `fig:"id" validate:"required"` - Regexp *regexp.Regexp `fig:"regexp" validate:"required"` - HostMatch *string `fig:"host_match"` - } `fig:"rule"` + Rule []Rule `fig:"rule"` +} + +// Rule represents a rule with its properties. +type Rule struct { + ID string `fig:"id" validate:"required"` + Regexp *regexp.Regexp `fig:"regexp" validate:"required"` + HostMatch *regexp.Regexp `fig:"host_match"` + Actions map[string]any `fig:"actions"` } // NewRuleset initializes a new Ruleset based on the provided Config. diff --git a/server.go b/server.go index 0a98107..b84ed4e 100644 --- a/server.go +++ b/server.go @@ -18,6 +18,9 @@ import ( "github.com/wneessen/go-parsesyslog" _ "github.com/wneessen/go-parsesyslog/rfc3164" _ "github.com/wneessen/go-parsesyslog/rfc5424" + + "github.com/wneessen/logranger/plugins/actions" + _ "github.com/wneessen/logranger/plugins/actions/all" ) const ( @@ -42,12 +45,28 @@ type Server struct { } // New creates a new instance of Server based on the provided Config -func New(c *Config) *Server { +func New(c *Config) (*Server, error) { s := &Server{ conf: c, } + s.setLogLevel() - return s + + if err := s.setRules(); err != nil { + return s, err + } + + p, err := parsesyslog.New(s.conf.internal.ParserType) + if err != nil { + return s, fmt.Errorf("failed to initialize syslog parser: %w", err) + } + s.parser = p + + if len(actions.Actions) <= 0 { + return s, fmt.Errorf("no action plugins found/configured") + } + + return s, nil } // Run starts the logranger Server by creating a new listener using the NewListener @@ -57,31 +76,6 @@ func (s *Server) Run() error { if err != nil { return err } - - p, err := parsesyslog.New(s.conf.internal.ParserType) - if err != nil { - return fmt.Errorf("failed to initialize syslog parser: %w", err) - } - s.parser = p - - rs, err := NewRuleset(s.conf) - if err != nil { - return fmt.Errorf("failed to read ruleset: %w", err) - } - s.ruleset = rs - for _, r := range rs.Rule { - s.log.Debug("found rule", slog.String("ID", r.ID)) - if r.HostMatch != nil { - s.log.Debug("host match enabled", slog.String("host", *r.HostMatch)) - } - if r.Regexp != nil { - foo := r.Regexp.FindAllStringSubmatch("test_foo23", -1) - if len(foo) > 0 { - s.log.Debug("matched", slog.Any("groups", foo)) - } - } - } - return s.RunWithListener(l) } @@ -177,14 +171,34 @@ ReadLoop: continue ReadLoop } } - s.log.Debug("log message successfully received", - slog.String("message", lm.Message.String()), - slog.String("facility", lm.Facility.String()), - slog.String("severity", lm.Severity.String()), - slog.Time("server_time", lm.Timestamp)) + if err = s.processMessage(lm); err != nil { + s.log.Error("failed to process actions on log message", LogErrKey, err) + } } } +func (s *Server) processMessage(lm parsesyslog.LogMsg) error { + if s.ruleset != nil { + for _, r := range s.ruleset.Rule { + if !r.Regexp.MatchString(lm.Message.String()) { + continue + } + if r.HostMatch != nil && !r.HostMatch.MatchString(lm.Hostname) { + continue + } + mg := r.Regexp.FindStringSubmatch(lm.Message.String()) + for n, a := range actions.Actions { + s.log.Debug("trying to execute action", slog.String("action_name", n)) + if err := a.Process(lm, mg, r.Actions); err != nil { + s.log.Error("failed to process action", LogErrKey, err, + slog.String("action", n), slog.String("rule_id", r.ID)) + } + } + } + } + return nil +} + // setLogLevel sets the log level based on the value of `s.conf.Log.Level`. // It creates a new `slog.HandlerOptions` and assigns the corresponding `slog.Level` // based on the value of `s.conf.Log.Level`. If the value is not one of the valid levels, @@ -209,3 +223,16 @@ func (s *Server) setLogLevel() { lh := slog.NewJSONHandler(os.Stdout, &lo) s.log = slog.New(lh).With(slog.String("context", "logranger")) } + +// setRules initializes/updates the ruleset for the logranger Server by +// calling NewRuleset with the config and assigns the returned ruleset +// to the Server's ruleset field. +// It returns an error if there is a failure in reading or loading the ruleset. +func (s *Server) setRules() error { + rs, err := NewRuleset(s.conf) + if err != nil { + return fmt.Errorf("failed to read ruleset: %w", err) + } + s.ruleset = rs + return nil +} diff --git a/template/template.go b/template/template.go new file mode 100644 index 0000000..ec6cecd --- /dev/null +++ b/template/template.go @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package template + +import ( + "fmt" + "strings" + "text/template" + "time" + + "github.com/wneessen/go-parsesyslog" +) + +// FuncMap represents a mapping of function names to their corresponding +// functions. +// It is used to define custom functions that can be accessed in Go +// templates. +type FuncMap struct{} + +// Compile compiles a template string using a given LogMsg, match group, +// and output template. +// It replaces special characters in the output template and creates a +// new template, named "template", with custom template functions from +// the FuncMap. It then populates a map with values from the LogMsg +// and current time and executes the template using the map as the +// data source. The compiled template result or an error is returned. +func Compile(lm parsesyslog.LogMsg, mg []string, ot string) (string, error) { + pt := strings.Builder{} + fm := NewTemplateFuncMap() + + ot = strings.ReplaceAll(ot, `\n`, "\n") + ot = strings.ReplaceAll(ot, `\t`, "\t") + ot = strings.ReplaceAll(ot, `\r`, "\r") + tpl, err := template.New("template").Funcs(fm).Parse(ot) + if err != nil { + return pt.String(), fmt.Errorf("failed to create template: %w", err) + } + + dm := make(map[string]any) + dm["match"] = mg + dm["hostname"] = lm.Hostname + dm["timestamp"] = lm.Timestamp + dm["now_rfc3339"] = time.Now().Format(time.RFC3339) + dm["now_unix"] = time.Now().Unix() + dm["severity"] = lm.Severity.String() + dm["facility"] = lm.Facility.String() + dm["appname"] = lm.AppName + dm["original_message"] = lm.Message + + if err = tpl.Execute(&pt, dm); err != nil { + return pt.String(), fmt.Errorf("failed to compile template: %w", err) + } + return pt.String(), nil +} + +// NewTemplateFuncMap creates a new template function map by returning a +// template.FuncMap. +func NewTemplateFuncMap() template.FuncMap { + fm := FuncMap{} + return template.FuncMap{ + "_ToLower": fm.ToLower, + } +} + +// ToLower returns a given string as lower-case representation +func (*FuncMap) ToLower(s string) string { + return strings.ToLower(s) +} + +// ToUpper returns a given string as upper-case representation +func (*FuncMap) ToUpper(s string) string { + return strings.ToUpper(s) +}