mirror of
https://github.com/wneessen/logranger.git
synced 2024-12-22 18:10:39 +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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
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
|
||||
}
|
15
rule.go
15
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.
|
||||
|
|
91
server.go
91
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
|
||||
}
|
||||
|
|
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