mirror of
https://github.com/wneessen/logranger.git
synced 2024-11-22 21:00:50 +01:00
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:
parent
6987f4627c
commit
b6f6b6a664
10 changed files with 278 additions and 40 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -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
18
plugins/action.go
Normal 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
|
||||||
|
}
|
5
plugins/actions/all/all.go
Normal file
5
plugins/actions/all/all.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package all
|
7
plugins/actions/all/file.go
Normal file
7
plugins/actions/all/file.go
Normal 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
|
71
plugins/actions/file/file.go
Normal file
71
plugins/actions/file/file.go
Normal 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{})
|
||||||
|
}
|
17
plugins/actions/registry.go
Normal file
17
plugins/actions/registry.go
Normal 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
11
rule.go
|
@ -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.
|
||||||
|
|
91
server.go
91
server.go
|
@ -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
75
template/template.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue