Implement Actions interface and update rule handling in Server

Introduced an Actions interface for plugins in 'action.go' and implemented a corresponding registry in 'registry.go'. Additionally, adjusted rule related behavior in 'Server' to account for actions, with relevant new fields in Ruleset and Rule. This enables multiple actions on a log message based on defined rules and further modularises the codebase, paving the path for addition of more plugin actions.
This commit is contained in:
Winni Neessen 2023-12-23 20:29:38 +01:00
parent 6987f4627c
commit b6f6b6a664
Signed by: wneessen
GPG key ID: 5F3AF39B820C119D
10 changed files with 278 additions and 40 deletions

View file

@ -35,10 +35,15 @@ func main() {
os.Exit(1) 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() { go func() {
if err = s.Run(); err != nil { 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) os.Exit(1)
} }
}() }()

View file

@ -6,6 +6,8 @@ package logranger
import ( import (
"bufio" "bufio"
"fmt"
"math/rand"
"net" "net"
) )
@ -23,9 +25,15 @@ type Connection struct {
func NewConnection(nc net.Conn) *Connection { func NewConnection(nc net.Conn) *Connection {
c := &Connection{ c := &Connection{
conn: nc, conn: nc,
id: "foo", id: NewConnectionID(),
rb: bufio.NewReader(nc), rb: bufio.NewReader(nc),
wb: bufio.NewWriter(nc), wb: bufio.NewWriter(nc),
} }
return c 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())
}

18
plugins/action.go Normal file
View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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
}

View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package all

View file

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package all
import _ "github.com/wneessen/logranger/plugins/actions/file" // register plugin

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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{})
}

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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
}

11
rule.go
View file

@ -14,12 +14,17 @@ import (
"github.com/kkyr/fig" "github.com/kkyr/fig"
) )
// Ruleset represents a collection of rules.
type Ruleset struct { type Ruleset struct {
Rule []struct { Rule []Rule `fig:"rule"`
}
// Rule represents a rule with its properties.
type Rule struct {
ID string `fig:"id" validate:"required"` ID string `fig:"id" validate:"required"`
Regexp *regexp.Regexp `fig:"regexp" validate:"required"` Regexp *regexp.Regexp `fig:"regexp" validate:"required"`
HostMatch *string `fig:"host_match"` HostMatch *regexp.Regexp `fig:"host_match"`
} `fig:"rule"` Actions map[string]any `fig:"actions"`
} }
// NewRuleset initializes a new Ruleset based on the provided Config. // NewRuleset initializes a new Ruleset based on the provided Config.

View file

@ -18,6 +18,9 @@ import (
"github.com/wneessen/go-parsesyslog" "github.com/wneessen/go-parsesyslog"
_ "github.com/wneessen/go-parsesyslog/rfc3164" _ "github.com/wneessen/go-parsesyslog/rfc3164"
_ "github.com/wneessen/go-parsesyslog/rfc5424" _ "github.com/wneessen/go-parsesyslog/rfc5424"
"github.com/wneessen/logranger/plugins/actions"
_ "github.com/wneessen/logranger/plugins/actions/all"
) )
const ( const (
@ -42,12 +45,28 @@ type Server struct {
} }
// New creates a new instance of Server based on the provided Config // 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{ s := &Server{
conf: c, conf: c,
} }
s.setLogLevel() 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 // 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 { if err != nil {
return err 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) return s.RunWithListener(l)
} }
@ -177,13 +171,33 @@ ReadLoop:
continue ReadLoop continue ReadLoop
} }
} }
s.log.Debug("log message successfully received", if err = s.processMessage(lm); err != nil {
slog.String("message", lm.Message.String()), s.log.Error("failed to process actions on log message", LogErrKey, err)
slog.String("facility", lm.Facility.String()),
slog.String("severity", lm.Severity.String()),
slog.Time("server_time", lm.Timestamp))
} }
} }
}
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`. // 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` // It creates a new `slog.HandlerOptions` and assigns the corresponding `slog.Level`
@ -209,3 +223,16 @@ func (s *Server) setLogLevel() {
lh := slog.NewJSONHandler(os.Stdout, &lo) lh := slog.NewJSONHandler(os.Stdout, &lo)
s.log = slog.New(lh).With(slog.String("context", "logranger")) 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
}

75
template/template.go Normal file
View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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)
}