Compare commits

...

31 Commits

Author SHA1 Message Date
Martin/Geno 29fb1c0801
migrate from genofire to sum7 2019-08-08 15:51:55 +02:00
Martin/Geno 61e52bee8b Merge branch 'changexmpplib' into 'master'
Change XMPP lib

See merge request genofire/logmania!1
2019-07-17 22:31:15 +02:00
Martin/Geno b7c7e895b9 Change XMPP lib 2019-07-17 22:31:15 +02:00
Martin/Geno 0a4adad430
use db Notifier for defaults (with regex ...) 2019-06-30 09:46:52 +02:00
Martin/Geno d2ca796145
filter also default 2019-06-30 09:33:45 +02:00
Martin/Geno 0ebee067dc
cleanup xmpp + prio in sending 2019-06-21 05:23:42 +02:00
Martin/Geno 346f3544d5
try color 2019-06-21 03:21:27 +02:00
Martin/Geno 6d1bc2341a
[DOC] readme fix url to download 2019-06-20 14:54:32 +02:00
Martin/Geno 6d1d9df282
disable output of caller 2019-06-20 14:47:53 +02:00
Martin/Geno 0ddbdc8f80
add empty testing to get coverage 2019-06-20 10:41:29 +02:00
Martin/Geno 5726dc3de6
fix logger lib change 2019-06-20 10:37:39 +02:00
Martin/Geno ce7d4282ca
fix misspelling 2019-06-20 10:31:56 +02:00
Martin/Geno 49950ebf28
ci to own default format 2019-06-20 10:21:19 +02:00
Martin/Geno 840142fc1d
change logging lib 2019-06-20 09:30:27 +02:00
Martin/Geno a5b0b06b3b
drop websocket and http support for hook2xmpp 2019-06-20 09:29:29 +02:00
Martin/Geno 89b426f3a9
[TASK] add gitlab-ci 2019-02-14 04:51:07 +01:00
Martin/Geno d84fd1cfbd
try gitlab-ci 2018-11-06 01:36:07 +01:00
Martin/Geno 5c906df45d
[TASK] refactory Bot 2018-09-12 01:31:53 +02:00
Martin/Geno 5f9ad29483
[BUGFIX] init notify with debug 2018-09-11 20:20:58 +02:00
Martin/Geno be29dede66
[BUGFIX] join xmpp on startup 2018-09-11 20:09:59 +02:00
Martin/Geno dcd0bd0e8f
use '.' instatt '!' as bot bash 2018-09-07 13:35:33 +02:00
Martin/Geno 2d8949dc69
[TASK] improve log message in webhooks 2018-09-06 13:42:29 +02:00
Martin/Geno 2155f245b7
[BUGFIX] do not crash on invalid input 2018-09-06 13:42:06 +02:00
Martin/Geno 1a129edd21
[BUGFIX] do not answer of history (+ improve logging) 2018-09-06 13:40:56 +02:00
Martin/Geno 73c4a38940
[TASK] add debugging 2018-09-06 13:36:25 +02:00
Martin/Geno 65047a6c8b
[TASK] add webhook support 2018-09-05 21:00:02 +02:00
Martin/Geno a9039fa290
[TASK] refactory to input and output 2018-09-05 01:53:23 +02:00
Martin/Geno d3318177aa
[TASK] add support for default (not configurated notify) 2018-09-04 22:11:20 +02:00
Martin/Geno ce46bad745
add notify to different files 2018-05-18 17:00:21 +02:00
Martin/Geno 64dbedf0c8
add websocket notify 2018-05-18 14:14:53 +02:00
Martin/Geno 627255139f
add regex replace in notify message 2018-05-18 14:07:44 +02:00
64 changed files with 1724 additions and 1163 deletions

8
.ci/check-gofmt Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
result="$(gofmt -s -l . | grep -v '^vendor/' )"
if [ -n "$result" ]; then
echo "Go code is not formatted, run 'gofmt -s -w .'" >&2
echo "$result"
exit 1
fi

25
.ci/check-testfiles Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# checks if every desired package has test files
import os
import re
import sys
source_re = re.compile(".*\.go")
test_re = re.compile(".*_test\.go")
missing = False
for root, dirs, files in os.walk("."):
# ignore some paths
if root == "." or root.startswith("./vendor") or root.startswith("./."):
continue
# source files but not test files?
if len(filter(source_re.match, files)) > 0 and len(filter(test_re.match, files)) == 0:
print("no test files for {}".format(root))
missing = True
if missing:
sys.exit(1)
else:
print("every package has test files")

View File

@ -1,6 +1,6 @@
workspace:
base: /go
path: src/dev.sum7.eu/genofire/logmania
path: src/dev.sum7.eu/sum7/logmania
pipeline:
build:

39
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,39 @@
image: golang:latest
stages:
- build
- test
before_script:
- mkdir -p "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cp -R "$CI_PROJECT_DIR" "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cd "/go/src/dev.sum7.eu/$CI_PROJECT_PATH"
- go get -d -t ./...
build-my-project:
stage: build
script:
- mkdir "$CI_PROJECT_DIR/bin/"
- go install "dev.sum7.eu/$CI_PROJECT_PATH"
- mv "/go/bin/$CI_PROJECT_NAME" "$CI_PROJECT_DIR/bin/$CI_PROJECT_NAME"
artifacts:
paths:
- "bin/$CI_PROJECT_NAME"
- config_example.toml
test-my-project:
stage: test
script:
- go get github.com/client9/misspell/cmd/misspell
- misspell -error .
- ./.ci/check-gofmt
- ./.ci/check-testfiles
- go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt
- go tool cover -func=.testCoverage.txt
artifacts:
paths:
- .testCoverage.txt
test-race-my-project:
stage: test
script:
- go test -race ./...

View File

@ -1,33 +0,0 @@
#!/bin/bash
# Issue: https://github.com/mattn/goveralls/issues/20
# Source: https://github.com/uber/go-torch/blob/63da5d33a225c195fea84610e2456d5f722f3963/.test-cover.sh
CI=$1
echo "run for $CI"
if [ "$CI" == "circle-ci" ]; then
cd ${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}
fi
echo "mode: count" > profile.cov
FAIL=0
# Standard go tooling behavior is to ignore dirs with leading underscors
for dir in $(find . -maxdepth 10 -not -path './vendor/*' -not -path './.git*' -not -path '*/_*' -type d);
do
if ls $dir/*.go &> /dev/null; then
go test -v -covermode=count -coverprofile=profile.tmp $dir || FAIL=$?
if [ -f profile.tmp ]
then
tail -n +2 < profile.tmp >> profile.cov
rm profile.tmp
fi
fi
done
# Failures have incomplete results, so don't send
if [ "$FAIL" -eq 0 ]; then
goveralls -v -coverprofile=profile.cov -service=$CI -repotoken=$COVERALLS_REPO_TOKEN
bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN -f profile.cov
fi
exit $FAIL

View File

@ -1,6 +1,52 @@
# logmania
[![DroneCI](https://ci.sum7.eu/api/badges/genofire/logmania/status.svg?branch=master)](https://ci.sum7.eu/genofire/logmania)
[![CircleCI](https://circleci.com/gh/genofire/logmania/tree/master.svg?style=shield)](https://circleci.com/gh/genofire/logmania/tree/master)
[![Coverage Status](https://coveralls.io/repos/github/genofire/logmania/badge.svg?branch=master)](https://coveralls.io/github/genofire/logmania?branch=master)
[![Go Report Card](https://goreportcard.com/badge/dev.sum7.eu/genofire/logmania)](https://goreportcard.com/report/dev.sum7.eu/genofire/logmania)
[![GoDoc](https://godoc.org/dev.sum7.eu/genofire/logmania?status.svg)](https://godoc.org/dev.sum7.eu/genofire/logmania)
[![pipeline status](https://dev.sum7.eu/sum7/logmania/badges/master/pipeline.svg)](https://dev.sum7.eu/genofire/logmania/pipelines)
[![coverage report](https://dev.sum7.eu/sum7/logmania/badges/master/coverage.svg)](https://dev.sum7.eu/genofire/logmania/pipelines)
[![Go Report Card](https://goreportcard.com/badge/dev.sum7.eu/sum7/logmania)](https://goreportcard.com/report/dev.sum7.eu/genofire/logmania)
[![GoDoc](https://godoc.org/dev.sum7.eu/sum7/logmania?status.svg)](https://godoc.org/dev.sum7.eu/genofire/logmania)
This is a little logging server.
## input
It receive logs (events) by:
- syslog
- journald (with service nc)
## output
And forward this logs (events) to multiple different output:
- xmpp (client and muc)
- file
there a multi filter possible
- regex
- priority
it could replace text by regex expression
configuration live possible by bot (on input e.g. xmpp)
## Get logmania
#### Download
Latest Build binary from ci here:
[Download All](https://dev.sum7.eu/sum7/logmania/-/jobs/artifacts/master/download/?job=build-my-project) (with config example)
[Download Binary](https://dev.sum7.eu/sum7/logmania/-/jobs/artifacts/master/raw/bin/logmania?inline=false&job=build-my-project)
#### Build
```bash
go get -u dev.sum7.eu/sum7/logmania
```
## Configure
see `config_example.toml`
## Related Projects
- [hook2xmpp](https://dev.sum7.eu/sum7/hook2xmpp) for e.g. grafana, alertmanager(prometheus), gitlab, git, circleci

View File

@ -2,257 +2,45 @@ package bot
import (
"fmt"
"strings"
timeago "github.com/ararog/timeago"
log "github.com/sirupsen/logrus"
)
type commandFunc func(func(string), string, []string)
type commandFunc func(from string, params []string) string
// list help
func (b *Bot) help(answer func(string), from string, params []string) {
msg := fmt.Sprintf("Hi %s there are the following commands:\n", from)
for _, cmd := range b.commands {
msg = fmt.Sprintf("%s - !%s\n", msg, cmd)
}
answer(msg)
type Command struct {
Name string
Description string
Commands []*Command
Action commandFunc
}
// add a chat to send log to a chat
func (b *Bot) addSend(answer func(string), from string, params []string) {
if len(params) < 1 {
answer("invalid: CMD IPAddress/Hostname\n or\n CMD IPAddress/Hostname to")
return
func (c Command) Run(from string, args []string) string {
if len(args) > 0 {
cmdName := args[0]
if cmdName == "help" {
return c.Help()
}
host := params[0]
to := from
if len(params) > 1 {
to = params[1]
if len(c.Commands) > 0 {
for _, cmd := range c.Commands {
if cmd.Name == cmdName {
return cmd.Run(from, args[1:])
}
h := b.db.GetHost(host)
if h == nil {
h = b.db.NewHost(host)
}
n, ok := b.db.NotifiesByAddress[to]
if !ok {
n = b.db.NewNotify(to)
return fmt.Sprintf("command %s not found\n%s", cmdName, c.Help())
}
h.AddNotify(n)
answer(fmt.Sprintf("added %s in list of %s", to, host))
}
if c.Action != nil {
return c.Action(from, args)
}
return c.Help()
}
//add a chat to send log to a chat
func (b *Bot) delSend(answer func(string), from string, params []string) {
if len(params) < 1 {
answer("invalid: CMD IPAddress/Hostname\n or\n CMD IPAddress/Hostname to")
return
func (c Command) Help() string {
if len(c.Commands) > 0 {
msg := fmt.Sprintf("%s\n-------------------", c.Description)
for _, cmd := range c.Commands {
msg = fmt.Sprintf("%s\n%s: %s", msg, cmd.Name, cmd.Description)
}
host := params[0]
to := from
if len(params) > 1 {
to = params[1]
return msg
}
if h := b.db.GetHost(host); h != nil {
h.DeleteNotify(to)
answer(fmt.Sprintf("removed %s in list of %s", to, host))
} else {
answer("not found host")
}
}
// list all hostname with the chat where it send to
func (b *Bot) listSend(answer func(string), from string, params []string) {
msg := "sending:\n"
all := false
of := from
if len(params) > 0 {
if params[0] == "all" {
all = true
} else {
of = params[0]
}
}
for _, host := range b.db.Hosts {
toList := ""
show := all
for _, to := range host.Notifies {
if all {
toList = fmt.Sprintf("%s , %s", toList, to)
} else if to == of {
show = true
}
}
if !show {
continue
}
if len(toList) > 3 {
toList = toList[3:]
}
if host.Name != "" {
msg = fmt.Sprintf("%s%s (%s): %s\n", msg, host.Address, host.Name, toList)
} else {
msg = fmt.Sprintf("%s%s: %s\n", msg, host.Address, toList)
}
}
answer(msg)
}
// add hostname
func (b *Bot) addHostname(answer func(string), from string, params []string) {
if len(params) < 2 {
answer("invalid: CMD IPAddress/Hostname NewHostname")
return
}
addr := params[0]
name := params[1]
h := b.db.GetHost(addr)
if h == nil {
h = b.db.NewHost(addr)
}
b.db.ChangeHostname(h, name)
answer(fmt.Sprintf("set for %s the hostname %s", addr, name))
}
func (b *Bot) delHostname(answer func(string), from string, params []string) {
if len(params) < 2 {
answer("invalid: CMD IPAddress/Hostname")
return
}
addr := params[0]
h := b.db.GetHost(addr)
if h != nil {
b.db.DeleteHost(h)
if h.Name != "" {
answer(fmt.Sprintf("remove host %s with hostname %s", h.Address, h.Name))
} else {
answer(fmt.Sprintf("remove host %s", h.Address))
}
} else {
answer("could not found host")
}
}
// list all host with his ip
func (b *Bot) listHostname(answer func(string), from string, params []string) {
msg := "hostnames:\n"
for _, host := range b.db.Hosts {
if host.Lastseen.Year() > 1 {
got, _ := timeago.TimeAgoFromNowWithTime(host.Lastseen)
msg = fmt.Sprintf("%s%s - %s (%s)\n", msg, host.Address, host.Name, got)
} else {
msg = fmt.Sprintf("%s%s - %s\n", msg, host.Address, host.Name)
}
}
answer(msg)
}
// set a filter by max
func (b *Bot) listMaxfilter(answer func(string), from string, params []string) {
msg := "filters: "
if len(params) > 0 && params[0] == "all" {
msg = fmt.Sprintf("%s\n", msg)
for _, n := range b.db.Notifies {
msg = fmt.Sprintf("%s%s - %s\n", msg, n.Address(), n.MaxPrioIn.String())
}
} else {
of := from
if len(params) > 0 {
of = params[0]
}
if filter, ok := b.db.NotifiesByAddress[of]; ok {
msg = fmt.Sprintf("%s of %s is %s", msg, of, filter)
}
}
answer(msg)
}
// set a filter to a mix
func (b *Bot) setMaxfilter(answer func(string), from string, params []string) {
if len(params) < 1 {
answer("invalid: CMD Priority\n or\n CMD Channel Priority")
return
}
to := from
var max log.Level
var err error
if len(params) > 1 {
to = params[0]
max, err = log.ParseLevel(params[1])
} else {
max, err = log.ParseLevel(params[0])
}
if err != nil {
answer("invalid priority: CMD Priority\n or\n CMD Channel Priority")
return
}
n, ok := b.db.NotifiesByAddress[to]
if !ok {
n = b.db.NewNotify(to)
}
n.MaxPrioIn = max
answer(fmt.Sprintf("set filter for %s to %s", to, max.String()))
}
// list of regex filter
func (b *Bot) listRegex(answer func(string), from string, params []string) {
msg := "regexs:\n"
if len(params) > 0 && params[0] == "all" {
for _, n := range b.db.Notifies {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, n.Address())
for expression := range n.RegexIn {
msg = fmt.Sprintf("%s - %s\n", msg, expression)
}
}
} else {
of := from
if len(params) > 0 {
of = params[0]
}
if n, ok := b.db.NotifiesByAddress[of]; ok {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, of)
for expression := range n.RegexIn {
msg = fmt.Sprintf("%s - %s\n", msg, expression)
}
}
}
answer(msg)
}
// add a regex filter
func (b *Bot) addRegex(answer func(string), from string, params []string) {
if len(params) < 1 {
answer("invalid: CMD regex\n or\n CMD channel regex")
return
}
regex := strings.Join(params, " ")
n := b.db.NotifiesByAddress[from]
if err := n.AddRegex(regex); err == nil {
answer(fmt.Sprintf("add regex for \"%s\" to %s", from, regex))
} else {
answer(fmt.Sprintf("\"%s\" is no valid regex expression: %s", regex, err))
}
}
// del a regex filter
func (b *Bot) delRegex(answer func(string), from string, params []string) {
if len(params) < 1 {
answer("invalid: CMD regex\n or\n CMD channel regex")
return
}
n := b.db.NotifiesByAddress[from]
regex := strings.Join(params, " ")
delete(n.RegexIn, regex)
b.listRegex(answer, from, []string{})
return c.Description
}

97
bot/filter.go Normal file
View File

@ -0,0 +1,97 @@
package bot
import (
"fmt"
"dev.sum7.eu/sum7/logmania/database"
)
func NewFilter(db *database.DB) *Command {
return &Command{
Name: "filter",
Description: "list and configure regex filter for channel by message content",
Commands: []*Command{
{
Name: "add",
Description: "add regex filter for channel: [channel] regex",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: [channel] regex"
}
of := from
regex := params[0]
if len(params) > 1 {
of = params[0]
regex = params[1]
}
n := db.NotifiesByAddress[of]
if err := n.AddRegex(regex); err != nil {
return fmt.Sprintf("\"%s\" is no valid regex expression: %s", regex, err)
}
return fmt.Sprintf("add regex for \"%s\" to %s", of, regex)
},
},
{
Name: "del",
Description: "del regex filter for channel: [channel] regex",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: [channel] regex"
}
of := from
regex := params[0]
if len(params) > 1 {
of = params[0]
regex = params[1]
}
n := db.NotifiesByAddress[of]
delete(n.RegexIn, regex)
return "deleted"
},
},
{
Name: "all",
Description: "list of all channels",
Action: func(from string, params []string) string {
msg := "filter:\n"
for _, n := range db.Notifies {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, n.Address())
for expression := range n.RegexIn {
msg = fmt.Sprintf("%s - %s\n", msg, expression)
}
}
return msg
},
},
{
Name: "channel",
Description: "list of given channel: channel",
Action: func(from string, params []string) string {
msg := "filter:\n"
if len(params) != 1 {
return "invalid: no channel given"
}
of := params[0]
if n, ok := db.NotifiesByAddress[of]; ok {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, of)
for expression := range n.RegexIn {
msg = fmt.Sprintf("%s - %s\n", msg, expression)
}
}
return msg
},
},
},
Action: func(from string, params []string) string {
msg := "filter:\n"
if n, ok := db.NotifiesByAddress[from]; ok {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, from)
for expression := range n.RegexIn {
msg = fmt.Sprintf("%s - %s\n", msg, expression)
}
}
return msg
},
}
}

67
bot/hostname.go Normal file
View File

@ -0,0 +1,67 @@
package bot
import (
"fmt"
"dev.sum7.eu/sum7/logmania/database"
timeago "github.com/ararog/timeago"
)
func NewHostname(db *database.DB) *Command {
return &Command{
Name: "hostname",
Description: "alternative short (host)names for long IP-Addresses or URLs (and time of last received input)",
Commands: []*Command{
{
Name: "set",
Description: "set or replace a hostname: IPAddress/Hostname NewHostname",
Action: func(from string, params []string) string {
if len(params) != 2 {
return "invalid: IPAddress/Hostname NewHostname"
}
addr := params[0]
name := params[1]
h := db.GetHost(addr)
if h == nil {
h = db.NewHost(addr)
}
db.ChangeHostname(h, name)
return fmt.Sprintf("set for %s the hostname %s", addr, name)
},
},
{
Name: "del",
Description: "delete a hostname: IPAddress/Hostname",
Action: func(from string, params []string) string {
if len(params) != 1 {
return "invalid: IPAddress/Hostname"
}
addr := params[0]
h := db.GetHost(addr)
if h != nil {
db.DeleteHost(h)
if h.Name != "" {
return fmt.Sprintf("remove host %s with hostname %s", h.Address, h.Name)
}
return fmt.Sprintf("remove host %s", h.Address)
}
return "could not found host"
},
},
},
Action: func(from string, params []string) string {
msg := "hostnames:\n"
for _, host := range db.Hosts {
if host.Lastseen.Year() > 1 {
got, _ := timeago.TimeAgoFromNowWithTime(host.Lastseen)
msg = fmt.Sprintf("%s%s - %s (%s)\n", msg, host.Address, host.Name, got)
} else {
msg = fmt.Sprintf("%s%s - %s\n", msg, host.Address, host.Name)
}
}
return msg
},
}
}

View File

@ -1,51 +1,36 @@
package bot
import (
"fmt"
"strings"
"github.com/mattn/go-shellwords"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/sum7/logmania/database"
)
type Bot struct {
db *database.DB
commandsMap map[string]commandFunc
commands []string
Command
}
func NewBot(db *database.DB) *Bot {
b := &Bot{
db: db,
}
b.commandsMap = map[string]commandFunc{
"help": b.help,
"send-add": b.addSend,
"send-list": b.listSend,
"send-del": b.delSend,
"hostname-set": b.addHostname,
"hostname-list": b.listHostname,
"hostname-del": b.delHostname,
"filter-set": b.setMaxfilter,
"filter-list": b.listMaxfilter,
"regex-add": b.addRegex,
"regex-list": b.listRegex,
"regex-del": b.delRegex,
}
for k := range b.commandsMap {
b.commands = append(b.commands, k)
}
return b
return &Bot{Command{
Description: "logmania bot, to configure live all settings",
Commands: []*Command{
NewFilter(db),
NewHostname(db),
NewPriority(db),
NewReplace(db),
NewSend(db),
},
}}
}
func (b *Bot) Handle(answer func(string), from, msg string) {
msgParts := strings.Split(msg, " ")
if len(msgParts[0]) <= 0 || msgParts[0][0] != '!' {
return
func (b *Bot) Handle(from, msg string) string {
msgParts, err := shellwords.Parse(msg)
if err != nil {
return ""
}
cmdName := msgParts[0][1:]
if cmd, ok := b.commandsMap[cmdName]; ok {
cmd(answer, from, msgParts[1:])
} else {
answer(fmt.Sprintf("not found command: !%s", cmdName))
if len(msgParts) <= 0 || msgParts[0][0] != '.' {
return ""
}
msgParts[0] = msgParts[0][1:]
return b.Run(from, msgParts)
}

82
bot/priority.go Normal file
View File

@ -0,0 +1,82 @@
package bot
import (
"fmt"
"github.com/bdlm/log"
logstd "github.com/bdlm/std/logger"
"dev.sum7.eu/sum7/logmania/database"
)
func NewPriority(db *database.DB) *Command {
return &Command{
Name: "priority",
Description: "list and configure priority in channel",
Commands: []*Command{
{
Name: "set",
Description: "set max priority of channel: [channel] Priority",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: [channel] Priority"
}
to := from
var max logstd.Level
var err error
if len(params) > 1 {
to = params[0]
max, err = log.ParseLevel(params[1])
} else {
max, err = log.ParseLevel(params[0])
}
if err != nil {
return "invalid: [Channel] Priority"
}
n, ok := db.NotifiesByAddress[to]
if !ok {
n = db.NewNotify(to)
}
n.MaxPrioIn = max
return fmt.Sprintf("set filter for %s to %s", to, log.LevelString(max))
},
},
{
Name: "all",
Description: "list of all channels",
Action: func(from string, params []string) string {
msg := "priority: \n"
for _, n := range db.Notifies {
msg = fmt.Sprintf("%s%s - %s\n", msg, n.Address(), log.LevelString(n.MaxPrioIn))
}
return msg
},
},
{
Name: "channel",
Description: "list of given channel: channel",
Action: func(from string, params []string) string {
if len(params) != 1 {
return "invalid: no channel given"
}
of := params[0]
msg := "priority: \n"
if notify, ok := db.NotifiesByAddress[of]; ok {
msg = fmt.Sprintf("%s %s is %s", msg, of, log.LevelString(notify.MaxPrioIn))
}
return msg
},
},
},
Action: func(from string, params []string) string {
msg := "priority: \n"
if notify, ok := db.NotifiesByAddress[from]; ok {
msg = fmt.Sprintf("%s %s is %s", msg, from, log.LevelString(notify.MaxPrioIn))
}
return msg
},
}
}

101
bot/replace.go Normal file
View File

@ -0,0 +1,101 @@
package bot
import (
"fmt"
"dev.sum7.eu/sum7/logmania/database"
)
func NewReplace(db *database.DB) *Command {
return &Command{
Name: "replace",
Description: "list and configure replace content of message for channel",
Commands: []*Command{
{
Name: "add",
Description: "add regex replace for channel: [channel] regex replace",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: [channel] regex replace"
}
of := from
regex := params[0]
value := params[1]
if len(params) > 2 {
of = params[0]
regex = params[1]
value = params[2]
}
n := db.NotifiesByAddress[of]
if err := n.AddRegexReplace(regex, value); err != nil {
return fmt.Sprintf("\"%s\" to \"%s\" is no valid regex replace expression: %s", regex, value, err)
}
return fmt.Sprintf("add replace in \"%s\" for \"%s\" to \"%s\"", of, regex, value)
},
},
{
Name: "del",
Description: "del regex replace for channel: [channel] regex replace",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: [channel] regex replace"
}
of := from
regex := params[0]
if len(params) > 1 {
of = params[0]
regex = params[1]
}
n := db.NotifiesByAddress[of]
delete(n.RegexReplace, regex)
return "deleted"
},
},
{
Name: "all",
Description: "list of all channels",
Action: func(from string, params []string) string {
msg := "replaces:\n"
for _, n := range db.Notifies {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, n.Address())
for expression, value := range n.RegexReplace {
msg = fmt.Sprintf("%s - \"%s\" : \"%s\"\n", msg, expression, value)
}
}
return msg
},
},
{
Name: "channel",
Description: "list of given channel: channel",
Action: func(from string, params []string) string {
if len(params) != 1 {
return "invalid: no channel given"
}
of := params[0]
msg := "replaces:\n"
if n, ok := db.NotifiesByAddress[of]; ok {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, of)
for expression, value := range n.RegexReplace {
msg = fmt.Sprintf("%s - \"%s\" : \"%s\"\n", msg, expression, value)
}
}
return msg
},
},
},
Action: func(from string, params []string) string {
msg := "replaces:\n"
if n, ok := db.NotifiesByAddress[from]; ok {
msg = fmt.Sprintf("%s%s\n-------------\n", msg, from)
for expression, value := range n.RegexReplace {
msg = fmt.Sprintf("%s - \"%s\" : \"%s\"\n", msg, expression, value)
}
}
return msg
},
}
}

142
bot/send.go Normal file
View File

@ -0,0 +1,142 @@
package bot
import (
"fmt"
"dev.sum7.eu/sum7/logmania/database"
)
func NewSend(db *database.DB) *Command {
return &Command{
Name: "send",
Description: "list and configure destination for hostnames",
Commands: []*Command{
{
Name: "add",
Description: "add a destination for host with: IPAddress/Hostname [to]",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: IPAddress/Hostname [to]"
}
host := params[0]
to := from
if len(params) > 1 {
to = params[1]
}
h := db.GetHost(host)
if h == nil {
h = db.NewHost(host)
}
if h == nil {
return fmt.Sprintf("could not create host %s", host)
}
n, ok := db.NotifiesByAddress[to]
if !ok {
n = db.NewNotify(to)
}
if n == nil {
return fmt.Sprintf("could not create notify %s in list of %s", to, host)
}
h.AddNotify(n)
return fmt.Sprintf("added %s in list of %s", to, host)
},
},
{
Name: "del",
Description: "del a destination for host with: IPAddress/Hostname [to]",
Action: func(from string, params []string) string {
if len(params) < 1 {
return "invalid: IPAddress/Hostname [to]"
}
host := params[0]
to := from
if len(params) > 1 {
to = params[1]
}
if h := db.GetHost(host); h != nil {
h.DeleteNotify(to)
return fmt.Sprintf("removed %s in list of %s", to, host)
}
return "not found host"
},
},
{
Name: "all",
Description: "list of all hosts with there channels",
Action: func(from string, params []string) string {
msg := "sending:\n"
for _, host := range db.Hosts {
toList := ""
for _, to := range host.Notifies {
toList = fmt.Sprintf("%s , %s", toList, to)
}
if len(toList) > 3 {
toList = toList[3:]
}
if host.Name != "" {
msg = fmt.Sprintf("%s%s (%s): %s\n", msg, host.Address, host.Name, toList)
} else {
msg = fmt.Sprintf("%s%s: %s\n", msg, host.Address, toList)
}
}
return msg
},
},
{
Name: "channel",
Description: "list all host of given channel: channel",
Action: func(from string, params []string) string {
if len(params) != 1 {
return "invalid: no channel given"
}
of := params[0]
msg := "sending:\n"
for _, host := range db.Hosts {
show := false
for _, to := range host.Notifies {
if to == of {
show = true
break
}
}
if !show {
continue
}
if host.Name != "" {
msg = fmt.Sprintf("%s%s (%s)\n", msg, host.Address, host.Name)
} else {
msg = fmt.Sprintf("%s%s\n", msg, host.Address)
}
}
return msg
},
},
},
Action: func(from string, params []string) string {
msg := "sending:\n"
for _, host := range db.Hosts {
show := false
for _, to := range host.Notifies {
if to == from {
show = true
break
}
}
if !show {
continue
}
if host.Name != "" {
msg = fmt.Sprintf("%s%s (%s)\n", msg, host.Address, host.Name)
} else {
msg = fmt.Sprintf("%s%s\n", msg, host.Address)
}
}
return msg
},
}
}

1
bot/send_test.go Normal file
View File

@ -0,0 +1 @@
package bot

View File

@ -1,41 +0,0 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:latest
working_directory: /go/src/dev.sum7.eu/genofire/logmania
steps:
- checkout
- run: go get -t -d -v ./...
- run: go install dev.sum7.eu/genofire/logmania
- store_artifacts:
path: /go/bin/
destination: logmania
test:
docker:
- image: circleci/golang:latest
working_directory: /go/src/dev.sum7.eu/genofire/logmania
steps:
- checkout
- run: go get -t -d -v ./...
- run: go get github.com/mattn/goveralls
- run: go get golang.org/x/tools/cmd/cover
- run: ./.test-coverage circle-ci
- store_test_results:
path: ./
destination: profile.cov
test_race:
docker:
- image: circleci/golang:latest
working_directory: /go/src/dev.sum7.eu/genofire/logmania
steps:
- checkout
- run: go get -t -d -v ./...
- run: go test -race ./...
workflows:
version: 2
build_and_tests:
jobs:
- build
- test
- test_race

View File

@ -1,7 +1,7 @@
package cmd
import (
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
"github.com/spf13/cobra"
)

1
cmd/root_test.go Normal file
View File

@ -0,0 +1 @@
package cmd

View File

@ -1,24 +1,23 @@
package cmd
import (
"net/http"
"os"
"os/signal"
"syscall"
"time"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
"github.com/spf13/cobra"
"dev.sum7.eu/genofire/golang-lib/file"
"dev.sum7.eu/genofire/golang-lib/worker"
"dev.sum7.eu/genofire/logmania/bot"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/notify"
allNotify "dev.sum7.eu/genofire/logmania/notify/all"
"dev.sum7.eu/genofire/logmania/receive"
allReceiver "dev.sum7.eu/genofire/logmania/receive/all"
"dev.sum7.eu/sum7/logmania/bot"
"dev.sum7.eu/sum7/logmania/database"
"dev.sum7.eu/sum7/logmania/input"
allInput "dev.sum7.eu/sum7/logmania/input/all"
"dev.sum7.eu/sum7/logmania/lib"
"dev.sum7.eu/sum7/logmania/output"
allOutput "dev.sum7.eu/sum7/logmania/output/all"
)
var (
@ -26,8 +25,8 @@ var (
config *lib.Config
db *database.DB
dbSaveWorker *worker.Worker
notifier notify.Notifier
receiver receive.Receiver
out output.Output
in input.Input
logChannel chan *log.Entry
logmaniaBot *bot.Bot
)
@ -46,40 +45,33 @@ var serverCmd = &cobra.Command{
if config == nil || err != nil {
log.Panicf("Could not load '%s' for configuration.", configPath)
}
if config.Debug {
log.SetLevel(log.DebugLevel)
}
db = database.ReadDBFile(config.DB)
go func() { dbSaveWorker = file.NewSaveJSONWorker(time.Minute, config.DB, db) }()
logmaniaBot = bot.NewBot(db)
notifier = allNotify.Init(&config.Notify, db, logmaniaBot)
out = allOutput.Init(config.Output, db, logmaniaBot)
logChannel = make(chan *log.Entry)
go func() {
for a := range logChannel {
notifier.Send(a)
out.Send(a, nil)
}
}()
if config.Notify.AlertCheck.Duration > time.Duration(time.Second) {
go db.Alert(config.Notify.AlertCheck.Duration, notifier.Send)
if config.AlertCheck.Duration > time.Duration(time.Second) {
go db.Alert(config.AlertCheck.Duration, out.Send)
}
log.Info("starting logmania")
log.WithField("defaults", len(db.DefaultNotify)).Info("starting logmania")
receiver = allReceiver.Init(&config.Receive, logChannel)
in = allInput.Init(config.Input, logChannel)
go receiver.Listen()
srv := &http.Server{
Addr: config.HTTPAddress,
}
go func() {
if err := srv.ListenAndServe(); err != nil {
panic(err)
}
}()
go in.Listen()
// Wait for system signal
sigchan := make(chan os.Signal, 1)
@ -103,8 +95,8 @@ var serverCmd = &cobra.Command{
func quit() {
dbSaveWorker.Close()
file.SaveJSON(config.DB, db)
receiver.Close()
notifier.Close()
in.Close()
out.Close()
log.Info("quit of logmania")
os.Exit(0)
}
@ -117,12 +109,17 @@ func reload() {
log.Errorf("reload: could not load '%s' for new configuration. Skip reload.", configPath)
return
}
receiver.Close()
receiver = allReceiver.Init(&config.Receive, logChannel)
go receiver.Listen()
if config.Debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
in.Close()
in = allInput.Init(config.Input, logChannel)
go in.Listen()
notifier.Close()
notifier = allNotify.Init(&config.Notify, db, logmaniaBot)
out.Close()
out = allOutput.Init(config.Output, db, logmaniaBot)
}
func init() {

31
config_example.toml Normal file
View File

@ -0,0 +1,31 @@
debug = false
database = "/tmp/logmania.state.json"
# have to be mote then a minute
alert_check = "5m"
#########
# Input #
#########
[input.syslog]
type = "udp"
address = ":10001"
[input.journald_json]
type = "udp"
address = ":10002"
##########
# Output #
##########
[output.file]
directory = "/tmp/"
default = "raw"
[output.xmpp]
jid = "user@example.org"
password = "password"
# if boolean is true for muc either user chat
default = { "log-raw@conference.example.org" = true, "person@example.org" = false }

View File

@ -89,6 +89,7 @@ func (db *DB) NewHost(addr string) *Host {
h := &Host{
Address: addr,
NotifiesByAddress: make(map[string]*Notify),
Lastseen: time.Now(),
}
db.AddHost(h)
return h

View File

@ -4,15 +4,17 @@ import (
"regexp"
"time"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
logstd "github.com/bdlm/std/logger"
)
const AlertMsg = "alert service from logmania, device did not send new message for a while"
type DB struct {
// depraced Format -> transformation to new format by db.update()
Hostname map[string]string `json:"hostname,omitempty"`
HostTo map[string]map[string]bool `json:"host_to,omitempty"`
MaxPrioIn map[string]log.Level `json:"maxLevel,omitempty"`
MaxPrioIn map[string]logstd.Level `json:"maxLevel,omitempty"`
RegexIn map[string]map[string]*regexp.Regexp `json:"regexIn,omitempty"`
Lastseen map[string]time.Time `json:"lastseen,omitempty"`
LastseenNotify map[string]time.Time `json:"-"`
@ -22,6 +24,7 @@ type DB struct {
HostsByName map[string]*Host `json:"-"`
Notifies []*Notify `json:"notifies"`
NotifiesByAddress map[string]*Notify `json:"-"`
DefaultNotify []*Notify `json:"-"`
}
func (db *DB) SendTo(e *log.Entry) (*log.Entry, *Host, []*Notify) {
@ -35,45 +38,43 @@ func (db *DB) SendTo(e *log.Entry) (*log.Entry, *Host, []*Notify) {
host.Lastseen = time.Now()
}
var toList []*Notify
for _, notify := range host.NotifiesByAddress {
if lvl := notify.MaxPrioIn; e.Level >= lvl {
continue
}
stopForTo := false
for _, expr := range notify.RegexIn {
if expr.MatchString(e.Message) {
stopForTo = true
continue
}
}
if stopForTo {
continue
}
toList = append(toList, notify)
}
entry := e
if host.Name != "" {
entry := e.WithField("hostname", host.Name)
entry = entry.WithField("hostname", host.Name)
entry.Level = e.Level
entry.Message = e.Message
}
// return default notify list
if host.Notifies == nil || len(host.Notifies) == 0 {
for _, notify := range db.DefaultNotify {
if notify.Send(e) {
toList = append(toList, notify)
}
}
} else {
// return with host specify list
for _, notify := range host.NotifiesByAddress {
if notify.Send(e) {
toList = append(toList, notify)
}
}
}
return entry, host, toList
}
return e, host, toList
} else {
host = db.NewHost(addr)
}
return e, host, nil
return e, host, db.DefaultNotify
}
func (db *DB) Alert(expired time.Duration, send func(e *log.Entry) error) {
func (db *DB) Alert(expired time.Duration, send func(e *log.Entry, n *Notify) bool) {
c := time.Tick(time.Minute)
for range c {
now := time.Now()
for _, h := range db.Hosts {
if !h.Lastseen.Before(now.Add(expired * -2)) {
if h.Lastseen.Before(now.Add(expired * -1)) {
continue
}
if h.LastseenNotify.Year() <= 1 && h.Lastseen.Before(h.LastseenNotify) {
if h.Lastseen.After(h.LastseenNotify) {
continue
}
h.LastseenNotify = now
@ -81,7 +82,7 @@ func (db *DB) Alert(expired time.Duration, send func(e *log.Entry) error) {
entry.Level = log.ErrorLevel
entry.Message = AlertMsg
entry.WithField("hostname", h.Address)
send(entry)
send(entry, nil)
}
}
}

1
database/main_test.go Normal file
View File

@ -0,0 +1 @@
package database

View File

@ -4,26 +4,41 @@ import (
"regexp"
"strings"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
logstd "github.com/bdlm/std/logger"
)
type Notify struct {
Protocoll string `json:"proto"`
Protocol string `json:"proto"`
To string `json:"to"`
RegexIn map[string]*regexp.Regexp `json:"regexIn"`
MaxPrioIn log.Level `json:"maxLevel"`
RegexReplace map[string]string `json:"regexReplace"`
MaxPrioIn logstd.Level `json:"maxLevel"`
regexReplaceExpression map[string]*regexp.Regexp
}
func (n *Notify) Init() {
if n.RegexIn == nil {
n.RegexIn = make(map[string]*regexp.Regexp)
}
if n.RegexReplace == nil {
n.RegexReplace = make(map[string]string)
}
if n.regexReplaceExpression == nil {
n.regexReplaceExpression = make(map[string]*regexp.Regexp)
}
for exp := range n.RegexIn {
regex, err := regexp.Compile(exp)
if err == nil {
n.RegexIn[exp] = regex
}
}
for exp := range n.RegexReplace {
regex, err := regexp.Compile(exp)
if err == nil {
n.regexReplaceExpression[exp] = regex
}
}
}
func (n *Notify) AddRegex(expression string) error {
@ -33,9 +48,36 @@ func (n *Notify) AddRegex(expression string) error {
}
return err
}
func (n *Notify) AddRegexReplace(expression, value string) error {
regex, err := regexp.Compile(expression)
if err == nil {
n.regexReplaceExpression[expression] = regex
n.RegexReplace[expression] = value
}
return err
}
func (n *Notify) RunReplace(msg string) string {
for key, re := range n.regexReplaceExpression {
value := n.RegexReplace[key]
msg = re.ReplaceAllString(msg, value)
}
return msg
}
func (n *Notify) Address() string {
return n.Protocoll + ":" + n.To
return n.Protocol + ":" + n.To
}
func (n *Notify) Send(e *log.Entry) bool {
if lvl := n.MaxPrioIn; e.Level >= lvl {
return false
}
for _, expr := range n.RegexIn {
if expr.MatchString(e.Message) {
return false
}
}
return true
}
// -- global notify
@ -49,7 +91,6 @@ func (db *DB) InitNotify() {
db.NotifiesByAddress[n.Address()] = n
}
}
func (db *DB) AddNotify(n *Notify) {
db.Notifies = append(db.Notifies, n)
db.NotifiesByAddress[n.Address()] = n
@ -57,10 +98,14 @@ func (db *DB) AddNotify(n *Notify) {
func (db *DB) NewNotify(to string) *Notify {
addr := strings.Split(to, ":")
if len(addr) != 2 {
return nil
}
n := &Notify{
Protocoll: addr[0],
Protocol: addr[0],
To: addr[1],
RegexIn: make(map[string]*regexp.Regexp),
MaxPrioIn: log.DebugLevel,
}
db.AddNotify(n)
return n

View File

@ -2,19 +2,19 @@ package database
import (
"dev.sum7.eu/genofire/golang-lib/file"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
)
func ReadDBFile(path string) *DB {
var db DB
if err := file.ReadJSON(path, &db); err == nil {
log.Infof("loaded %d hosts", len(db.HostTo))
db.InitNotify()
db.InitHost()
// import
db.update()
log.Infof("loaded %d hosts and %d notifies", len(db.Hosts), len(db.Notifies))
return &db
} else {
log.Error("failed to open db file: ", path, ":", err)

46
input/all/internal.go Normal file
View File

@ -0,0 +1,46 @@
package all
import (
"github.com/bdlm/log"
"dev.sum7.eu/sum7/logmania/input"
)
type Input struct {
input.Input
list []input.Input
}
func Init(configInterface interface{}, exportChannel chan *log.Entry) input.Input {
config := configInterface.(map[string]interface{})
var list []input.Input
for inputType, init := range input.Register {
configForItem := config[inputType]
if configForItem == nil {
log.Warnf("the input type '%s' has no configuration", inputType)
continue
}
in := init(configForItem, exportChannel)
if in == nil {
continue
}
list = append(list, in)
}
return &Input{
list: list,
}
}
func (in *Input) Listen() {
for _, item := range in.list {
go item.Listen()
}
}
func (in *Input) Close() {
for _, item := range in.list {
item.Close()
}
}

View File

@ -0,0 +1 @@
package all

6
input/all/main.go Normal file
View File

@ -0,0 +1,6 @@
package all
import (
_ "dev.sum7.eu/sum7/logmania/input/journald_json"
_ "dev.sum7.eu/sum7/logmania/input/syslog"
)

View File

@ -4,7 +4,8 @@ import (
"encoding/json"
"strconv"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
logstd "github.com/bdlm/std/logger"
)
type JournalMessage struct {
@ -34,7 +35,7 @@ type JournalMessage struct {
Message string `json:"MESSAGE"`
}
var PriorityMap = map[int]log.Level{
var PriorityMap = map[int]logstd.Level{
0: log.PanicLevel, // emerg
1: log.PanicLevel, // alert
2: log.PanicLevel, // crit

View File

@ -0,0 +1 @@
package journald_json

View File

@ -0,0 +1,77 @@
package journald_json
import (
"net"
"github.com/bdlm/log"
"github.com/mitchellh/mapstructure"
"dev.sum7.eu/sum7/logmania/input"
)
const inputType = "journald_json"
var logger = log.WithField("input", inputType)
type Input struct {
input.Input
exportChannel chan *log.Entry
serverSocket *net.UDPConn
}
type InputConfig struct {
Type string `mapstructure:"type"`
Address string `mapstructure:"address"`
}
func Init(configInterface interface{}, exportChannel chan *log.Entry) input.Input {
var config InputConfig
if err := mapstructure.Decode(configInterface, &config); err != nil {
logger.Warnf("not able to decode data: %s", err)
return nil
}
addr, err := net.ResolveUDPAddr(config.Type, config.Address)
ln, err := net.ListenUDP(config.Type, addr)
if err != nil {
logger.Error("init ", err)
return nil
}
in := &Input{
serverSocket: ln,
exportChannel: exportChannel,
}
logger.Info("init")
return in
}
const maxDataGramSize = 8192
func (in *Input) Listen() {
logger.Info("listen")
for {
buf := make([]byte, maxDataGramSize)
n, src, err := in.serverSocket.ReadFromUDP(buf)
if err != nil {
logger.Warn("failed to accept connection", err)
continue
}
raw := make([]byte, n)
copy(raw, buf)
entry := toLogEntry(raw, src.IP.String())
if entry != nil {
in.exportChannel <- entry
}
}
}
func (in *Input) Close() {
in.serverSocket.Close()
}
func init() {
input.Add(inputType, Init)
}

18
input/main.go Normal file
View File

@ -0,0 +1,18 @@
package input
import (
"github.com/bdlm/log"
)
var Register = make(map[string]Init)
type Input interface {
Listen()
Close()
}
type Init func(interface{}, chan *log.Entry) Input
func Add(name string, init Init) {
Register[name] = init
}

1
input/main_test.go Normal file
View File

@ -0,0 +1 @@
package input

View File

@ -1,12 +1,13 @@
package syslog
import (
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
logstd "github.com/bdlm/std/logger"
libSyslog "dev.sum7.eu/genofire/logmania/lib/syslog"
libSyslog "dev.sum7.eu/sum7/logmania/lib/syslog"
)
var SyslogPriorityMap = map[int]log.Level{
var SyslogPriorityMap = map[int]logstd.Level{
0: log.PanicLevel,
1: log.PanicLevel,
2: log.PanicLevel,

View File

@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
)
func TestToEntry(t *testing.T) {

77
input/syslog/main.go Normal file
View File

@ -0,0 +1,77 @@
package syslog
import (
"net"
"github.com/bdlm/log"
"github.com/mitchellh/mapstructure"
"dev.sum7.eu/sum7/logmania/input"
)
const inputType = "syslog"
var logger = log.WithField("input", inputType)
type Input struct {
input.Input
exportChannel chan *log.Entry
serverSocket *net.UDPConn
}
type InputConfig struct {
Type string `mapstructure:"type"`
Address string `mapstructure:"address"`
}
func Init(configInterface interface{}, exportChannel chan *log.Entry) input.Input {
var config InputConfig
if err := mapstructure.Decode(configInterface, &config); err != nil {
logger.Warnf("not able to decode data: %s", err)
return nil
}
addr, err := net.ResolveUDPAddr(config.Type, config.Address)
ln, err := net.ListenUDP(config.Type, addr)
if err != nil {
logger.Error("init ", err)
return nil
}
in := &Input{
serverSocket: ln,
exportChannel: exportChannel,
}
logger.Info("init")
return in
}
const maxDataGramSize = 8192
func (in *Input) Listen() {
logger.Info("listen")
for {
buf := make([]byte, maxDataGramSize)
n, src, err := in.serverSocket.ReadFromUDP(buf)
if err != nil {
logger.Warn("failed to accept connection", err)
continue
}
raw := make([]byte, n)
copy(raw, buf)
entry := toLogEntry(raw, src.IP.String())
if entry != nil {
in.exportChannel <- entry
}
}
}
func (in *Input) Close() {
in.serverSocket.Close()
}
func init() {
input.Add(inputType, Init)
}

View File

@ -1,31 +1,11 @@
package lib
// Struct of the configuration
// e.g. under dev.sum7.eu/genofire/logmania/logmania_example.conf
// e.g. under dev.sum7.eu/sum7/logmania/logmania_example.conf
type Config struct {
Notify NotifyConfig `toml:"notify"`
Receive ReceiveConfig `toml:"receive"`
Debug bool `toml:"debug"`
DB string `toml:"database"`
HTTPAddress string `toml:"http_address"`
}
type NotifyConfig struct {
AlertCheck Duration `toml:"alert_check"`
Console bool `toml:"debug"`
XMPP struct {
JID string `toml:"jid"`
Password string `toml:"password"`
} `toml:"xmpp"`
File string `toml:"file"`
}
type ReceiveConfig struct {
Syslog struct {
Type string `toml:"type"`
Address string `toml:"address"`
} `toml:"syslog"`
JournaldJSON struct {
Type string `toml:"type"`
Address string `toml:"address"`
} `toml:"journald_json"`
Output map[string]interface{} `toml:"output"`
Input map[string]interface{} `toml:"input"`
}

1
lib/syslog/main_test.go Normal file
View File

@ -0,0 +1 @@
package syslog

View File

@ -1,11 +0,0 @@
[notify]
state_file = "/tmp/logmania.state.json"
debug = true
[receive.syslog]
type = "udp"
address = ":10001"
[receive.journald_json]
type = "udp"
address = ":10002"

View File

@ -1,6 +1,6 @@
package main
import "dev.sum7.eu/genofire/logmania/cmd"
import "dev.sum7.eu/sum7/logmania/cmd"
func main() {
cmd.Execute()

View File

@ -1,54 +0,0 @@
package all
import (
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/bot"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/notify"
)
type Notifier struct {
notify.Notifier
list []notify.Notifier
channelNotify chan *log.Entry
}
func Init(config *lib.NotifyConfig, db *database.DB, bot *bot.Bot) notify.Notifier {
var list []notify.Notifier
for _, init := range notify.NotifyRegister {
notify := init(config, db, bot)
if notify == nil {
continue
}
list = append(list, notify)
}
n := &Notifier{
list: list,
channelNotify: make(chan *log.Entry),
}
go n.sender()
return n
}
func (n *Notifier) sender() {
for c := range n.channelNotify {
for _, item := range n.list {
item.Send(c)
}
}
}
func (n *Notifier) Send(e *log.Entry) error {
n.channelNotify <- e
return nil
}
func (n *Notifier) Close() {
for _, item := range n.list {
item.Close()
}
}

View File

@ -1,6 +0,0 @@
package all
import (
_ "dev.sum7.eu/genofire/logmania/notify/file"
_ "dev.sum7.eu/genofire/logmania/notify/xmpp"
)

View File

@ -1,65 +0,0 @@
package xmpp
import (
"os"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/bot"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/notify"
)
const (
proto = "file:"
)
var logger = log.WithField("notify", proto)
type Notifier struct {
notify.Notifier
formatter log.Formatter
file *os.File
path string
}
func Init(config *lib.NotifyConfig, db *database.DB, bot *bot.Bot) notify.Notifier {
logger.Info("startup")
if config.File == "" {
return nil
}
file, err := os.OpenFile(config.File, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
logger.Errorf("could not open file: %s", err.Error())
return nil
}
return &Notifier{
formatter: &log.JSONFormatter{},
file: file,
path: config.File,
}
}
func (n *Notifier) Send(e *log.Entry) error {
text, err := n.formatter.Format(e)
if err != nil {
return err
}
_, err = n.file.Write(text)
if err != nil {
logger.Warnf("could not write to logfile: %s - try to reopen it", err.Error())
file, err := os.OpenFile(n.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
n.file = file
_, err = n.file.Write(text)
}
return err
}
func init() {
notify.AddNotifier(Init)
}

View File

@ -1,22 +0,0 @@
package notify
import (
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/bot"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/genofire/logmania/lib"
)
var NotifyRegister []NotifyInit
type Notifier interface {
Send(entry *log.Entry) error
Close()
}
type NotifyInit func(*lib.NotifyConfig, *database.DB, *bot.Bot) Notifier
func AddNotifier(n NotifyInit) {
NotifyRegister = append(NotifyRegister, n)
}

View File

@ -1,205 +0,0 @@
package xmpp
import (
"errors"
xmpp_client "dev.sum7.eu/genofire/yaja/client"
xmpp "dev.sum7.eu/genofire/yaja/xmpp"
"dev.sum7.eu/genofire/yaja/xmpp/base"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/bot"
"dev.sum7.eu/genofire/logmania/database"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/notify"
)
const (
proto = "xmpp"
protoGroup = "xmpp-muc"
nickname = "logmania"
)
var logger = log.WithField("notify", proto)
type Notifier struct {
notify.Notifier
client *xmpp_client.Client
channels map[string]bool
db *database.DB
formatter log.Formatter
}
func Init(config *lib.NotifyConfig, db *database.DB, bot *bot.Bot) notify.Notifier {
channels := make(map[string]bool)
client, err := xmpp_client.NewClient(xmppbase.NewJID(config.XMPP.JID), config.XMPP.Password)
if err != nil {
logger.Error(err)
return nil
}
go func() {
for {
if err := client.Start(); err != nil {
log.Warn("close connection, try reconnect")
client.Connect(config.XMPP.Password)
} else {
log.Warn("closed connection")
return
}
}
}()
go func() {
for {
element, more := client.Recv()
if !more {
log.Warn("could not receive new message, try later")
continue
}
switch element.(type) {
case *xmpp.PresenceClient:
pres := element.(*xmpp.PresenceClient)
sender := pres.From
logPres := logger.WithField("from", sender.Full())
switch pres.Type {
case xmpp.PresenceTypeSubscribe:
logPres.Debugf("recv presence subscribe")
pres.Type = xmpp.PresenceTypeSubscribed
pres.To = sender
pres.From = nil
client.Send(pres)
logPres.Debugf("accept new subscribe")
pres.Type = xmpp.PresenceTypeSubscribe
pres.ID = ""
client.Send(pres)
logPres.Info("request also subscribe")
case xmpp.PresenceTypeSubscribed:
logPres.Info("recv presence accepted subscribe")
case xmpp.PresenceTypeUnsubscribe:
logPres.Info("recv presence remove subscribe")
case xmpp.PresenceTypeUnsubscribed:
logPres.Info("recv presence removed subscribe")
case xmpp.PresenceTypeUnavailable:
logPres.Debug("recv presence unavailable")
case "":
logPres.Debug("recv empty presence, maybe from joining muc")
continue
default:
logPres.Warnf("recv presence unsupported: %s -> %s", pres.Type, xmpp.XMLChildrenString(pres))
}
case *xmpp.MessageClient:
msg := element.(*xmpp.MessageClient)
from := msg.From.Bare().String()
if msg.Type == xmpp.MessageTypeGroupchat {
from = protoGroup + ":" + from
} else {
from = proto + ":" + from
}
bot.Handle(func(answer string) {
to := msg.From
if msg.Type == xmpp.MessageTypeGroupchat && !to.IsBare() {
to = to.Bare()
}
err := client.Send(&xmpp.MessageClient{
Type: msg.Type,
To: to,
Body: answer,
})
if err != nil {
logger.Error("xmpp to ", msg.From.String(), " error:", err)
}
}, from, msg.Body)
}
}
}()
for toAddr, toAddresses := range db.NotifiesByAddress {
if toAddresses.Protocoll == protoGroup {
toJID := xmppbase.NewJID(toAddresses.To)
toJID.Resource = nickname
err := client.Send(&xmpp.PresenceClient{
To: toJID,
})
if err != nil {
logger.Error("xmpp could not join ", toJID.String(), " error:", err)
} else {
channels[toAddr] = true
}
}
}
logger.Info("startup")
return &Notifier{
channels: channels,
client: client,
db: db,
formatter: &log.TextFormatter{
DisableTimestamp: true,
},
}
}
func (n *Notifier) Send(e *log.Entry) error {
e, _, tos := n.db.SendTo(e)
if tos == nil || len(tos) <= 0 {
return errors.New("no receiver found")
}
text, err := n.formatter.Format(e)
if err != nil {
return err
}
for _, to := range tos {
if to.Protocoll == protoGroup {
if _, ok := n.channels[to.To]; ok {
toJID := xmppbase.NewJID(to.To)
toJID.Resource = nickname
err := n.client.Send(&xmpp.PresenceClient{
To: toJID,
})
if err != nil {
logger.Error("xmpp could not join ", toJID.String(), " error:", err)
} else {
n.channels[to.To] = true
}
}
err := n.client.Send(&xmpp.MessageClient{
Type: xmpp.MessageTypeGroupchat,
To: xmppbase.NewJID(to.To),
Body: string(text),
})
if err != nil {
logger.Error("xmpp to ", to.To, " error:", err)
}
} else {
err := n.client.Send(&xmpp.MessageClient{
Type: xmpp.MessageTypeChat,
To: xmppbase.NewJID(to.To),
Body: string(text),
})
if err != nil {
logger.Error("xmpp to ", to, " error:", err)
}
}
}
return nil
}
func (n *Notifier) Close() {
for jid := range n.channels {
toJID := xmppbase.NewJID(jid)
toJID.Resource = nickname
err := n.client.Send(&xmpp.PresenceClient{
To: toJID,
Type: xmpp.PresenceTypeUnavailable,
})
if err != nil {
logger.Error("xmpp could not leave ", toJID.String(), " error:", err)
}
}
n.client.Close()
}
func init() {
notify.AddNotifier(Init)
}

93
output/all/internal.go Normal file
View File

@ -0,0 +1,93 @@
package all
import (
"github.com/bdlm/log"
"time"
"dev.sum7.eu/sum7/logmania/bot"
"dev.sum7.eu/sum7/logmania/database"
"dev.sum7.eu/sum7/logmania/output"
)
var logger = log.WithField("notify", "all")
type Output struct {
output.Output
list []output.Output
db *database.DB
channelNotify chan *log.Entry
}
func Init(configInterface interface{}, db *database.DB, bot *bot.Bot) output.Output {
config := configInterface.(map[string]interface{})
var list []output.Output
for outputType, init := range output.Register {
configForItem := config[outputType]
if configForItem == nil {
log.Warnf("the input type '%s' has no configuration\n", outputType)
continue
}
notify := init(configForItem, db, bot)
if notify == nil {
continue
}
list = append(list, notify)
def := notify.Default()
if def == nil {
continue
}
db.DefaultNotify = append(db.DefaultNotify, def...)
}
out := &Output{
db: db,
list: list,
channelNotify: make(chan *log.Entry),
}
go out.sender()
return out
}
func (out *Output) sender() {
for c := range out.channelNotify {
e, _, tos := out.db.SendTo(c)
for _, to := range tos {
send := false
for _, item := range out.list {
send = item.Send(e, to)
if send {
break
}
}
if !send {
logger.Warnf("notify not send to %s: [%d] %s", to.Address(), c.Level, c.Message)
}
}
}
}
func (out *Output) Send(e *log.Entry, to *database.Notify) bool {
before := time.Now()
logger := log.WithFields(e.Data)
logger = logger.WithField("msg", e.Message)
logger.Debugf("starting forward message")
out.channelNotify <- e
after := time.Now()
delta := after.Sub(before)
logger.WithField("ms", float64(delta)/float64(time.Millisecond)).Debugf("end forward message")
return true
}
func (out *Output) Close() {
for _, item := range out.list {
item.Close()
}
}

View File

@ -0,0 +1 @@
package all

6
output/all/main.go Normal file
View File

@ -0,0 +1,6 @@
package all
import (
_ "dev.sum7.eu/sum7/logmania/output/file"
_ "dev.sum7.eu/sum7/logmania/output/xmpp"
)

107
output/file/main.go Normal file
View File

@ -0,0 +1,107 @@
package file
import (
"os"
"path"
"regexp"
"github.com/bdlm/log"
"github.com/mitchellh/mapstructure"
"dev.sum7.eu/sum7/logmania/bot"
"dev.sum7.eu/sum7/logmania/database"
"dev.sum7.eu/sum7/logmania/output"
)
const (
proto = "file"
)
var logger = log.WithField("output", proto)
type Output struct {
output.Output
defaults []*database.Notify
files map[string]*os.File
formatter log.Formatter
path string
}
type OutputConfig struct {
Directory string `mapstructure:"directory"`
Default string `mapstructure:"default"`
}
func Init(configInterface interface{}, db *database.DB, bot *bot.Bot) output.Output {
var config OutputConfig
if err := mapstructure.Decode(configInterface, &config); err != nil {
logger.Warnf("not able to decode data: %s", err)
return nil
}
if config.Directory == "" {
return nil
}
logger.WithField("directory", config.Directory).Info("startup")
var defaults []*database.Notify
if config.Default != "" {
defaults = append(defaults, &database.Notify{
Protocol: proto,
To: config.Default,
RegexIn: make(map[string]*regexp.Regexp),
MaxPrioIn: log.DebugLevel,
})
}
return &Output{
defaults: defaults,
files: make(map[string]*os.File),
formatter: &log.JSONFormatter{
DisableCaller: true,
},
path: config.Directory,
}
}
func (out *Output) Default() []*database.Notify {
return out.defaults
}
func (out *Output) getFile(name string) *os.File {
if file, ok := out.files[name]; ok {
return file
}
if m, err := regexp.MatchString(`^[0-9A-Za-z_-]*$`, name); err != nil || !m {
logger.Errorf("not allowed to use '%s:%s'", proto, name)
return nil
}
filename := path.Join(out.path, name+".json")
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
logger.Errorf("could not open file: %s", err.Error())
return nil
}
out.files[name] = file
return file
}
func (out *Output) Send(e *log.Entry, to *database.Notify) bool {
if to.Protocol != proto {
return false
}
byteText, err := out.formatter.Format(e)
if err != nil {
return false
}
text := to.RunReplace(string(byteText))
file := out.getFile(to.To)
if file == nil {
return false
}
_, err = file.WriteString(text)
return err == nil
}
func init() {
output.Add(proto, Init)
}

1
output/file/main_test.go Normal file
View File

@ -0,0 +1 @@
package file

22
output/main.go Normal file
View File

@ -0,0 +1,22 @@
package output
import (
"github.com/bdlm/log"
"dev.sum7.eu/sum7/logmania/bot"
"dev.sum7.eu/sum7/logmania/database"
)
var Register = make(map[string]Init)
type Output interface {
Default() []*database.Notify
Send(entry *log.Entry, to *database.Notify) bool
Close()
}
type Init func(interface{}, *database.DB, *bot.Bot) Output
func Add(name string, init Init) {
Register[name] = init
}

1
output/main_test.go Normal file
View File

@ -0,0 +1 @@
package output

126
output/xmpp/format.go Normal file
View File

@ -0,0 +1,126 @@
package xmpp
import (
"bytes"
"fmt"
"text/template"
"github.com/bdlm/log"
)
var tempLog = template.Must(template.New("log").Parse(
"{{$color := .Color}}<span>" +
// Hostname
"{{ if .Hostname }}<span style=\"color: {{ $color.Hostname }};\">{{ .Hostname}}</span>{{ end }}" +
// Level
"<span style=\"font-weight: bold; color: {{$color.Level}};\">{{printf \" %5s\" .Level}}</span>" +
// Message
"{{printf \" %s\" .Message}}" +
// Data
"{{if .Data}}{{range $k, $v := .Data}}" +
"<span style=\"color: {{$color.DataLabel}};\">{{printf \" %s\" $k}}</span>" +
"=" +
"<span style=\"color: {{$color.DataValue}};\">{{$v}}</span>" +
"{{end}}{{end}}" +
"</span>",
))
var (
// DEFAULTColor is the default html 'level' color.
DEFAULTColor = "#00ff00"
// ERRORColor is the html 'level' color for error messages.
ERRORColor = "#ff8700"
// FATALColor is the html 'level' color for fatal messages.
FATALColor = "#af0000"
// PANICColor is the html 'level' color for panic messages.
PANICColor = "#ff0000"
// WARNColor is the html 'level' color for warning messages.
WARNColor = "#ffff00"
// DEBUGColor is the html 'level' color for debug messages.
DEBUGColor = "#8a8a8a"
// DataLabelColor is the html data label color.
DataLabelColor = "#87afff"
// DataValueColor is the html data value color.
DataValueColor = "#d7af87"
// HostnameColor is the html hostname color.
HostnameColor = "#00afff"
// TimestampColor is the html timestamp color.
TimestampColor = "#5faf87"
)
type logData struct {
Color colors `json:"-"`
Data map[string]interface{} `json:"data,omitempty"`
Hostname string `json:"host,omitempty"`
Level string `json:"level,omitempty"`
Message string `json:"msg,omitempty"`
Timestamp string `json:"time,omitempty"`
}
type colors struct {
DataLabel string
DataValue string
Hostname string
Level string
Reset string
Timestamp string
}
func formatLog(entry *log.Entry) (string, string) {
var levelColor string
var logLine *bytes.Buffer
if entry.Buffer != nil {
logLine = entry.Buffer
} else {
logLine = &bytes.Buffer{}
}
data := &logData{
Data: make(map[string]interface{}),
Level: log.LevelString(entry.Level),
Message: entry.Message,
Timestamp: entry.Time.Format(log.RFC3339Milli),
}
switch entry.Level {
case log.DebugLevel:
levelColor = DEBUGColor
case log.WarnLevel:
levelColor = WARNColor
case log.ErrorLevel:
levelColor = ERRORColor
case log.FatalLevel:
levelColor = FATALColor
case log.PanicLevel:
levelColor = PANICColor
default:
levelColor = DEFAULTColor
}
data.Color = colors{
DataLabel: DataLabelColor,
DataValue: DataValueColor,
Hostname: HostnameColor,
Level: levelColor,
Timestamp: TimestampColor,
}
for k, v := range entry.Data {
if k == "hostname" {
if data.Hostname == "" {
data.Hostname = v.(string)
}
continue
}
if str, ok := v.(string); ok {
data.Data[k] = "'" + str + "'"
} else {
data.Data[k] = v
}
}
if err := tempLog.Execute(logLine, data); err != nil {
return "formating error", "formating error"
}
return logLine.String(), fmt.Sprintf("[%s] %s > %s", data.Hostname, log.LevelString(entry.Level), entry.Message)
}

121
output/xmpp/main.go Normal file
View File

@ -0,0 +1,121 @@
package xmpp
import (
"regexp"
"github.com/bdlm/log"
"github.com/mitchellh/mapstructure"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"dev.sum7.eu/sum7/logmania/bot"
"dev.sum7.eu/sum7/logmania/database"
"dev.sum7.eu/sum7/logmania/output"
)
const (
proto = "xmpp"
protoGroup = "xmpp-muc"
nickname = "logmania"
)
var logger = log.WithField("output", proto)
type Output struct {
output.Output
defaults []*database.Notify
channels map[string]bool
bot *bot.Bot
client xmpp.Sender
botOut chan interface{}
logOut chan interface{}
}
type OutputConfig struct {
Address string `mapstructure:"address"`
JID string `mapstructure:"jid"`
Password string `mapstructure:"password"`
Defaults map[string]bool `mapstructure:"default"`
}
func Init(configInterface interface{}, db *database.DB, bot *bot.Bot) output.Output {
var config OutputConfig
if err := mapstructure.Decode(configInterface, &config); err != nil {
logger.Warnf("not able to decode data: %s", err)
return nil
}
out := &Output{
channels: make(map[string]bool),
bot: bot,
}
router := xmpp.NewRouter()
router.HandleFunc("message", out.recvMessage)
router.HandleFunc("presence", out.recvPresence)
client, err := xmpp.NewClient(xmpp.Config{
Address: config.Address,
Jid: config.JID,
Password: config.Password,
}, router)
if err != nil {
logger.Error(err)
return nil
}
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
out.client = c
for to, muc := range config.Defaults {
def := &database.Notify{
Protocol: proto,
To: to,
RegexIn: make(map[string]*regexp.Regexp),
MaxPrioIn: log.DebugLevel,
}
if muc {
def.Protocol = protoGroup
out.Join(to)
}
out.defaults = append(out.defaults, def)
}
for _, toAddresses := range db.NotifiesByAddress {
if toAddresses.Protocol == protoGroup {
out.Join(toAddresses.To)
}
}
logger.Info("join muc after connect")
})
go func() {
cm.Run()
log.Panic("closed connection")
}()
logger.WithField("jid", config.JID).Info("startup")
return out
}
func (out *Output) Default() []*database.Notify {
return out.defaults
}
func (out *Output) Close() {
for jid := range out.channels {
toJID, err := xmpp.NewJid(jid)
if err != nil {
logger.Error("xmpp could generate jid to leave ", jid, " error:", err)
}
toJID.Resource = nickname
if err = out.client.Send(stanza.Presence{Attrs: stanza.Attrs{
To: toJID.Full(),
Type: stanza.PresenceTypeUnavailable,
}}); err != nil {
logger.Error("xmpp could not leave ", toJID.Full(), " error:", err)
}
}
}
func init() {
output.Add(proto, Init)
}

1
output/xmpp/main_test.go Normal file
View File

@ -0,0 +1 @@
package xmpp

109
output/xmpp/recv.go Normal file
View File

@ -0,0 +1,109 @@
package xmpp
import (
"time"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func (out *Output) recvMessage(s xmpp.Sender, p stanza.Packet) {
before := time.Now()
msg, ok := p.(stanza.Message)
if !ok {
logger.Errorf("blame gosrc.io/xmpp for routing: %s", p)
return
}
logger.WithFields(map[string]interface{}{
"sender": msg.From,
"request": msg.Body,
}).Debug("handling bot message")
from, err := xmpp.NewJid(msg.From)
if err != nil {
logger.Errorf("blame gosrc.io/xmpp for jid encoding: %s", msg.From)
return
}
fromBare := from.Bare()
fromLogmania := ""
if msg.Type == stanza.MessageTypeGroupchat {
fromLogmania = protoGroup + ":" + fromBare
} else {
fromLogmania = proto + ":" + fromBare
}
answer := out.bot.Handle(fromLogmania, msg.Body)
if answer == "" {
return
}
if err := s.Send(stanza.Message{Attrs: stanza.Attrs{To: fromBare, Type: msg.Type}, Body: answer}); err != nil {
logger.WithFields(map[string]interface{}{
"sender": fromLogmania,
"request": msg.Body,
"answer": answer,
}).Errorf("unable to send bot answer: %s", err)
}
after := time.Now()
delta := after.Sub(before)
logger.WithFields(map[string]interface{}{
"sender": fromLogmania,
"request": msg.Body,
"answer": answer,
"ms": float64(delta) / float64(time.Millisecond),
}).Debug("handled xmpp bot message")
}
func (out *Output) recvPresence(s xmpp.Sender, p stanza.Packet) {
pres, ok := p.(stanza.Presence)
if !ok {
logger.Errorf("blame gosrc.io/xmpp for routing: %s", p)
return
}
from, err := xmpp.NewJid(pres.From)
if err != nil {
logger.Errorf("blame gosrc.io/xmpp for jid encoding: %s", pres.From)
return
}
fromBare := from.Bare()
logPres := logger.WithField("from", from)
switch pres.Type {
case stanza.PresenceTypeSubscribe:
logPres.Debugf("recv presence subscribe")
if err := s.Send(stanza.Presence{Attrs: stanza.Attrs{
Type: stanza.PresenceTypeSubscribed,
To: fromBare,
Id: pres.Id,
}}); err != nil {
logPres.WithField("user", pres.From).Errorf("answer of subscribe not send: %s", err)
return
}
logPres.Debugf("accept new subscribe")
if err := s.Send(stanza.Presence{Attrs: stanza.Attrs{
Type: stanza.PresenceTypeSubscribe,
To: fromBare,
}}); err != nil {
logPres.WithField("user", pres.From).Errorf("request of subscribe not send: %s", err)
return
}
logPres.Info("request also subscribe")
case stanza.PresenceTypeSubscribed:
logPres.Info("recv presence accepted subscribe")
case stanza.PresenceTypeUnsubscribe:
logPres.Info("recv presence remove subscribe")
case stanza.PresenceTypeUnsubscribed:
logPres.Info("recv presence removed subscribe")
case stanza.PresenceTypeUnavailable:
logPres.Debug("recv presence unavailable")
case "":
logPres.Debug("recv empty presence, maybe from joining muc")
return
default:
logPres.Warnf("recv presence unsupported: %s -> %v", pres.Type, pres)
}
}

79
output/xmpp/send.go Normal file
View File

@ -0,0 +1,79 @@
package xmpp
import (
"strings"
"github.com/bdlm/log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"dev.sum7.eu/sum7/logmania/database"
)
func (out *Output) Join(to string) {
toJID, err := xmpp.NewJid(to)
if err != nil {
logger.Errorf("jid not generate to join muc %s : %s", to, err)
return
}
toJID.Resource = nickname
if err = out.client.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
History: stanza.History{MaxStanzas: stanza.NewNullableInt(0)},
}},
}); err != nil {
logger.Errorf("muc not join %s : %s", toJID.Full(), err)
} else {
out.channels[to] = true
}
}
func (out *Output) Send(e *log.Entry, to *database.Notify) bool {
if out.client == nil {
logger.Error("xmpp not connected (yet)")
return false
}
html, text := formatLog(e)
if html == "" || text == "" {
logger.Error("during format notify")
return false
}
html = strings.TrimRight(to.RunReplace(html), "\n")
text = strings.TrimRight(to.RunReplace(text), "\n")
msg := stanza.Message{
Attrs: stanza.Attrs{
To: to.To,
},
Body: text,
Extensions: []stanza.MsgExtension{
stanza.HTML{Body: stanza.HTMLBody{InnerXML: html}},
},
}
if to.Protocol == protoGroup {
if _, ok := out.channels[to.To]; ok {
out.Join(to.To)
}
msg.Type = stanza.MessageTypeGroupchat
if err := out.client.Send(msg); err != nil {
logger.WithFields(map[string]interface{}{
"muc": to.To,
"text": text,
}).Errorf("log message not forwarded: %s", err)
}
return true
}
if to.Protocol == proto {
msg.Type = stanza.MessageTypeChat
if err := out.client.Send(msg); err != nil {
logger.WithFields(map[string]interface{}{
"user": to.To,
"text": text,
}).Errorf("log message not forwarded: %s", err)
}
return true
}
return false
}

View File

@ -1,40 +0,0 @@
package all
import (
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/receive"
)
type Receiver struct {
receive.Receiver
list []receive.Receiver
}
func Init(config *lib.ReceiveConfig, exportChannel chan *log.Entry) receive.Receiver {
var list []receive.Receiver
for _, init := range receive.Register {
receiver := init(config, exportChannel)
if receiver == nil {
continue
}
list = append(list, receiver)
}
return &Receiver{
list: list,
}
}
func (r *Receiver) Listen() {
for _, item := range r.list {
go item.Listen()
}
}
func (r *Receiver) Close() {
for _, item := range r.list {
item.Close()
}
}

View File

@ -1,7 +0,0 @@
package all
import (
_ "dev.sum7.eu/genofire/logmania/receive/journald_json"
_ "dev.sum7.eu/genofire/logmania/receive/logrus"
_ "dev.sum7.eu/genofire/logmania/receive/syslog"
)

View File

@ -1,65 +0,0 @@
package journald_json
import (
"net"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/receive"
)
var logger = log.WithField("receive", "journald_json")
type Receiver struct {
receive.Receiver
exportChannel chan *log.Entry
serverSocket *net.UDPConn
}
func Init(config *lib.ReceiveConfig, exportChannel chan *log.Entry) receive.Receiver {
addr, err := net.ResolveUDPAddr(config.JournaldJSON.Type, config.JournaldJSON.Address)
ln, err := net.ListenUDP(config.JournaldJSON.Type, addr)
if err != nil {
logger.Error("init ", err)
return nil
}
recv := &Receiver{
serverSocket: ln,
exportChannel: exportChannel,
}
logger.Info("init")
return recv
}
const maxDataGramSize = 8192
func (rc *Receiver) Listen() {
logger.Info("listen")
for {
buf := make([]byte, maxDataGramSize)
n, src, err := rc.serverSocket.ReadFromUDP(buf)
if err != nil {
logger.Warn("failed to accept connection", err)
continue
}
raw := make([]byte, n)
copy(raw, buf)
entry := toLogEntry(raw, src.IP.String())
if entry != nil {
rc.exportChannel <- entry
}
}
}
func (rc *Receiver) Close() {
rc.serverSocket.Close()
}
func init() {
receive.AddReceiver("journald_json", Init)
}

View File

@ -1,85 +0,0 @@
package client
import (
"io"
websocketLib "dev.sum7.eu/genofire/golang-lib/websocket"
"dev.sum7.eu/genofire/logmania/receive/logrus"
"github.com/google/uuid"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
// client logger
type Logmania struct {
URL string
Token uuid.UUID
Levels []log.Level
quere chan *log.Entry
conn *websocket.Conn
}
func NewClient(url string, token uuid.UUID, lvls ...log.Level) *Logmania {
logger := &Logmania{
URL: url,
Token: token,
Levels: lvls,
quere: make(chan *log.Entry),
}
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Error("[logmania] error on connect: ", err)
return nil
}
logger.conn = conn
go logger.Start()
return logger
}
// Listen if logmania server want to close the connection
func (l *Logmania) listen() {
for {
var msg websocketLib.Message
err := websocket.ReadJSON(l.conn, &msg)
if err == io.EOF {
l.Close()
log.Warn("[logmania] close listener:", err)
} else if err != nil {
log.Println(err)
} else {
if msg.Subject == websocketLib.SessionMessageInit {
l.conn.WriteJSON(&websocketLib.Message{
Subject: websocketLib.SessionMessageInit,
ID: l.Token,
})
}
}
}
}
func (l *Logmania) writer() {
for e := range l.quere {
err := l.conn.WriteJSON(&websocketLib.Message{
Subject: logrus.WS_LOG_ENTRY,
Body: e,
})
if err != nil {
log.Error("[logmania] could not send log entry:", err)
}
}
}
func (l *Logmania) Start() {
go l.listen()
l.writer()
}
func (l *Logmania) Fire(e *log.Entry) {
l.quere <- e
}
// close connection to logger
func (l *Logmania) Close() {
l.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
close(l.quere)
}

View File

@ -1 +0,0 @@
package client

View File

@ -1,55 +0,0 @@
package logrus
import (
"net/http"
"dev.sum7.eu/genofire/golang-lib/websocket"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/receive"
)
const WS_LOG_ENTRY = "log"
var logger = log.WithField("receive", "logrus")
type Receiver struct {
receive.Receiver
input chan *websocket.Message
exportChannel chan *log.Entry
serverSocket *websocket.Server
}
func Init(config *lib.ReceiveConfig, exportChannel chan *log.Entry) receive.Receiver {
inputMsg := make(chan *websocket.Message)
ws := websocket.NewServer(inputMsg, websocket.NewSessionManager())
http.HandleFunc("/receiver", ws.Handler)
recv := &Receiver{
input: inputMsg,
serverSocket: ws,
exportChannel: exportChannel,
}
logger.Info("init")
return recv
}
func (rc *Receiver) Listen() {
logger.Info("listen")
for msg := range rc.input {
if event, ok := msg.Body.(log.Entry); ok {
rc.exportChannel <- &event
}
}
}
func (rc *Receiver) Close() {
}
func init() {
receive.AddReceiver("websocket", Init)
}

View File

@ -1,19 +0,0 @@
package receive
import (
"dev.sum7.eu/genofire/logmania/lib"
log "github.com/sirupsen/logrus"
)
var Register = make(map[string]ReceiverInit)
type Receiver interface {
Listen()
Close()
}
type ReceiverInit func(*lib.ReceiveConfig, chan *log.Entry) Receiver
func AddReceiver(name string, n ReceiverInit) {
Register[name] = n
}

View File

@ -1,65 +0,0 @@
package syslog
import (
"net"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/logmania/lib"
"dev.sum7.eu/genofire/logmania/receive"
)
var logger = log.WithField("receive", "syslog")
type Receiver struct {
receive.Receiver
exportChannel chan *log.Entry
serverSocket *net.UDPConn
}
func Init(config *lib.ReceiveConfig, exportChannel chan *log.Entry) receive.Receiver {
addr, err := net.ResolveUDPAddr(config.Syslog.Type, config.Syslog.Address)
ln, err := net.ListenUDP(config.Syslog.Type, addr)
if err != nil {
logger.Error("init ", err)
return nil
}
recv := &Receiver{
serverSocket: ln,
exportChannel: exportChannel,
}
logger.Info("init")
return recv
}
const maxDataGramSize = 8192
func (rc *Receiver) Listen() {
logger.Info("listen")
for {
buf := make([]byte, maxDataGramSize)
n, src, err := rc.serverSocket.ReadFromUDP(buf)
if err != nil {
logger.Warn("failed to accept connection", err)
continue
}
raw := make([]byte, n)
copy(raw, buf)
entry := toLogEntry(raw, src.IP.String())
if entry != nil {
rc.exportChannel <- entry
}
}
}
func (rc *Receiver) Close() {
rc.serverSocket.Close()
}
func init() {
receive.AddReceiver("syslog", Init)
}