Compare commits

..

No commits in common. "master" and "statusctl" have entirely different histories.

26 changed files with 49 additions and 674 deletions

View file

@ -1 +0,0 @@
/Dockerfile

2
.gitignore vendored
View file

@ -1,2 +0,0 @@
.*.vim
/build

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "vendor/gopkg.in/mcuadros/go-syslog.v2"]
path = vendor/gopkg.in/mcuadros/go-syslog.v2
url = https://github.com/boyska/go-syslog

View file

@ -1,13 +0,0 @@
FROM golang:1.12
ENV GOOS=linux
ENV CGO_ENABLED=0
RUN mkdir -p $GOPATH/src/git.lattuga.net/boyska/circolog/
COPY . $GOPATH/src/git.lattuga.net/boyska/circolog/
RUN go get git.lattuga.net/boyska/circolog/...
VOLUME [$GOPATH"/bin"]
ENTRYPOINT ["/go/bin/circologd"]
CMD []

View file

@ -107,12 +107,3 @@ message you are filtering out will still consume space in memory (and will be av
Filtering brings big dependencies, which will add some 5-6 megabytes to circolog binaries. If you want to Filtering brings big dependencies, which will add some 5-6 megabytes to circolog binaries. If you want to
avoid it, install with `go install -tags nofilter git.lattuga.net/boyska/circolog/...` and your binaries will avoid it, install with `go install -tags nofilter git.lattuga.net/boyska/circolog/...` and your binaries will
be a bit smaller. be a bit smaller.
## Develop
To tinker with circolog, there is also a `Dockerfile`. Simply:
```
$ docker build -t circolog .
$ docker run -t circolog
```

View file

@ -11,12 +11,12 @@ import (
"strconv" "strconv"
"time" "time"
"git.lattuga.net/boyska/circolog/data"
"git.lattuga.net/boyska/circolog/filtering" "git.lattuga.net/boyska/circolog/filtering"
"git.lattuga.net/boyska/circolog/formatter" "git.lattuga.net/boyska/circolog/formatter"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
"github.com/mgutz/ansi" "github.com/mgutz/ansi"
"gopkg.in/mcuadros/go-syslog.v2/format"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
@ -54,17 +54,14 @@ func (b *BoolAuto) Set(s string) error {
} }
func main() { func main() {
queryAddr := flag.String("addr", "", "http service address") addr := flag.String("addr", "localhost:9080", "http service address")
querySocket := flag.String("socket", "/tmp/circologd-query.sock", "Path to a unix domain socket for the HTTP server") querySocket := flag.String("socket", "", "Path to a unix domain socket for the HTTP server")
backlogLimit := flag.Int("n", -1, "Limit the backlog length, defaults to no limit (-1)") backlogLimit := flag.Int("n", -1, "Limit the backlog length, defaults to no limit (-1)")
var format formatter.Format
format = formatter.FormatSyslog
flag.Var(&format, "fmt", "Output format [syslog|json]")
var filter filtering.ExprValue var filter filtering.ExprValue
flag.Var(&filter, "where", "sql-like query to filter logs") flag.Var(&filter, "where", "sql-like query to filter logs")
// TODO: change to color-mode=auto/no/always // TODO: change to color-mode=auto/no/always
hasColor := BoolAuto_AUTO hasColor := BoolAuto_AUTO
flag.Var(&hasColor, "color", "dis/enable colors; yes/no/auto") flag.Var(&hasColor, "color", "dis/enable colors")
flag.Parse() flag.Parse()
if hasColor == BoolAuto_NO || (!isatty.IsTerminal(os.Stdout.Fd()) && hasColor != BoolAuto_YES) { if hasColor == BoolAuto_NO || (!isatty.IsTerminal(os.Stdout.Fd()) && hasColor != BoolAuto_YES) {
@ -75,7 +72,7 @@ func main() {
signal.Notify(interrupt, os.Interrupt) signal.Notify(interrupt, os.Interrupt)
var d *websocket.Dialer var d *websocket.Dialer
u := url.URL{Scheme: "ws", u := url.URL{Scheme: "ws",
Host: *queryAddr, // ignored in case of -socket; see the Dialer below Host: *addr, // ignored in case of -socket; see the Dialer below
Path: "/ws", Path: "/ws",
} }
q := u.Query() q := u.Query()
@ -84,7 +81,7 @@ func main() {
q.Set("l", strconv.Itoa(*backlogLimit)) q.Set("l", strconv.Itoa(*backlogLimit))
} }
u.RawQuery = q.Encode() u.RawQuery = q.Encode()
if *queryAddr == "" { if *querySocket != "" {
d = &websocket.Dialer{ d = &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) { NetDial: func(network, addr string) (net.Conn, error) {
return net.Dial("unix", *querySocket) return net.Dial("unix", *querySocket)
@ -95,7 +92,7 @@ func main() {
log.Printf("connecting to %s", *querySocket) log.Printf("connecting to %s", *querySocket)
} else { } else {
d = websocket.DefaultDialer d = websocket.DefaultDialer
log.Printf("connecting to %s", *queryAddr) log.Printf("connecting to %s", *addr)
} }
c, _, err := d.Dial(u.String(), nil) c, _, err := d.Dial(u.String(), nil)
@ -115,7 +112,7 @@ func main() {
log.Println("close:", err) log.Println("close:", err)
return return
} }
var parsed data.Message var parsed format.LogParts
if err := bson.Unmarshal(serialized, &parsed); err != nil { if err := bson.Unmarshal(serialized, &parsed); err != nil {
log.Println("invalid BSON", err) log.Println("invalid BSON", err)
continue continue
@ -123,12 +120,10 @@ func main() {
if !filter.Validate(parsed) { if !filter.Validate(parsed) {
continue continue
} }
if err := formatter.WriteFormatted(os.Stdout, format, parsed); err != nil { if err := formatter.WriteFormatted(os.Stdout, formatter.FormatSyslog, parsed); err != nil {
log.Println("error printing", err) log.Println("error printing", err)
} }
if format == formatter.FormatSyslog { // oops fmt.Println()
fmt.Println()
}
} }
}() }()
@ -149,7 +144,7 @@ func main() {
select { select {
case <-done: case <-done:
log.Println("Successfully close") log.Println("Successfully close")
case <-time.After(5 * time.Second): case <-time.After(1 * time.Second):
log.Println("Forced close") log.Println("Forced close")
} }
return return

View file

@ -1,39 +0,0 @@
package main
import (
"net"
"github.com/coreos/go-systemd/activation"
)
func Listeners() ([]net.Listener, error) {
files := activation.Files(false)
listeners := make([]net.Listener, len(files))
for i, f := range files {
if pc, err := net.FileListener(f); err == nil {
listeners[i] = pc
f.Close()
}
}
return listeners, nil
}
// PacketConns returns a slice containing a net.PacketConn for each matching socket type
// passed to this process.
//
// The order of the file descriptors is preserved in the returned slice.
// Nil values are used to fill any gaps. For example if systemd were to return file descriptors
// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn}
func PacketConns() ([]net.PacketConn, error) {
files := activation.Files(false)
conns := make([]net.PacketConn, len(files))
for i, f := range files {
if pc, err := net.FilePacketConn(f); err == nil {
conns[i] = pc
f.Close()
}
}
return conns, nil
}

View file

@ -9,9 +9,9 @@ import (
"time" "time"
"git.lattuga.net/boyska/circolog" "git.lattuga.net/boyska/circolog"
"git.lattuga.net/boyska/circolog/data"
"git.lattuga.net/boyska/circolog/formatter" "git.lattuga.net/boyska/circolog/formatter"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"gopkg.in/mcuadros/go-syslog.v2/format"
) )
func setupHTTP(hub circolog.Hub) *http.ServeMux { func setupHTTP(hub circolog.Hub) *http.ServeMux {
@ -100,7 +100,7 @@ func getHTTPHandler(hub circolog.Hub) http.HandlerFunc {
opts.Nofollow = true opts.Nofollow = true
client := circolog.Client{ client := circolog.Client{
Messages: make(chan data.Message, 20), Messages: make(chan format.LogParts, 20),
Options: opts, Options: opts,
} }
hub.Register <- client hub.Register <- client
@ -141,7 +141,7 @@ func getWSHandler(hub circolog.Hub) http.HandlerFunc {
return return
} }
client := circolog.Client{ client := circolog.Client{
Messages: make(chan data.Message, 20), Messages: make(chan format.LogParts, 20),
Options: opts, Options: opts,
} }
hub.Register <- client hub.Register <- client
@ -153,15 +153,6 @@ func getWSHandler(hub circolog.Hub) http.HandlerFunc {
hub.Unregister <- c hub.Unregister <- c
conn.Close() conn.Close()
}() }()
go func() {
for {
_, _, err := conn.ReadMessage()
if err != nil {
conn.Close()
return
}
}
}()
for { for {
select { select {
case message, ok := <-c.Messages: case message, ok := <-c.Messages:

View file

@ -9,39 +9,26 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"git.lattuga.net/boyska/circolog" "git.lattuga.net/boyska/circolog"
"git.lattuga.net/boyska/circolog/formatter"
"github.com/coreos/go-systemd/daemon"
syslog "gopkg.in/mcuadros/go-syslog.v2" syslog "gopkg.in/mcuadros/go-syslog.v2"
) )
var socketsToRemove []string
func cleanSocket(socket string) { func cleanSocket(socket string) {
if err := os.Remove(socket); err != nil { if err := os.Remove(socket); err != nil {
fmt.Fprintln(os.Stderr, "Error cleaning", socket, ":", err) fmt.Fprintln(os.Stderr, "Error cleaning", socket, ":", err)
} }
} }
func removeAtExit(socket string) {
socketsToRemove = append(socketsToRemove, socket)
}
func main() { func main() {
var err error var err error
var syslogSocket SyslogSocket syslogSocketPath := flag.String("syslogd-socket", "", "The socket to listen to syslog addresses")
var logFmt formatter.SyslogRFC
logFmt.Format = syslog.Automatic
flag.Var(&syslogSocket, "syslogd-socket", "The socket to listen to syslog addresses")
// dumpSocketPath := flag.String("dump-socket", "/run/buffer.sock", "The socket that user will connect to in order to receive logs") // dumpSocketPath := flag.String("dump-socket", "/run/buffer.sock", "The socket that user will connect to in order to receive logs")
bufsize := flag.Int("buffer-size", 1000, "Number of messages to keep") bufsize := flag.Int("buffer-size", 1000, "Number of messages to keep")
syslogAddr := flag.String("syslog-addr", "127.0.0.1:9514", "Address:port where to listen for syslog messages") syslogAddr := flag.String("syslog-addr", "127.0.0.1:9514", "Address:port where to listen for syslog messages")
queryAddr := flag.String("query-addr", "", "Address:port where to bind the query service") queryAddr := flag.String("query-addr", "127.0.0.1:9080", "Address:port where to bind the query service")
querySocket := flag.String("query-socket", "/tmp/circologd-query.sock", "Path to a unix domain socket for the HTTP server; recommended for security reasons!") querySocket := flag.String("query-socket", "", "Path to a unix domain socket for the HTTP server; recommended for security reasons!")
ctlSocket := flag.String("ctl-socket", "/tmp/circologd-ctl.sock", "Path to a unix domain socket for the control server; leave empty to disable") ctlSocket := flag.String("ctl-socket", "/tmp/circologd-ctl.sock", "Path to a unix domain socket for the control server; leave empty to disable")
flag.Var(&logFmt, "log-fmt", "Log messages format. If not set, defaults to automatic choice. Allowed values: rfc3164, rfc5424, auto.")
verbose := flag.Bool("verbose", false, "Print more output executing the daemon") verbose := flag.Bool("verbose", false, "Print more output executing the daemon")
debug := flag.Bool("debug", false, "Print debugging info executing the daemon") debug := flag.Bool("debug", false, "Print debugging info executing the daemon")
flag.Parse() flag.Parse()
@ -54,33 +41,20 @@ func main() {
go hub.Run() go hub.Run()
server := syslog.NewServer() server := syslog.NewServer()
server.SetFormat(logFmt.Format) server.SetFormat(syslog.RFC5424)
fmt.Printf("Syslog format set to: %s\n", logFmt.String())
server.SetHandler(handler) server.SetHandler(handler)
if syslogSocket.isSocketActivated { if *syslogSocketPath != "" {
fmt.Printf("Binding to socket `%s` [syslog]\n", syslogSocket.String()) if err = server.ListenUnixgram(*syslogSocketPath); err != nil {
if syslogSocket.Listener != nil { fmt.Fprintln(os.Stderr, "argh", err)
fmt.Println("(stream)") os.Exit(1)
server.Listen(syslogSocket.Listener)
} else {
fmt.Println("(datagram)", syslogSocket.Conn)
server.ListenDgram(syslogSocket.Conn)
} }
defer cleanSocket(*syslogSocketPath)
fmt.Printf("Binding socket `%s` [syslog]\n", *syslogSocketPath)
} else { } else {
syslogSocketPath := syslogSocket.Path fmt.Printf("Binding address `%s` [syslog]\n", *syslogAddr)
if syslogSocketPath != "" { if err = server.ListenUDP(*syslogAddr); err != nil {
if err = server.ListenUnixgram(syslogSocketPath); err != nil { fmt.Fprintln(os.Stderr, "argh", err)
fmt.Fprintln(os.Stderr, "argh", err) os.Exit(1)
os.Exit(1)
}
fmt.Printf("Binding socket `%s` [syslog]\n", syslogSocketPath)
removeAtExit(syslogSocketPath)
} else {
fmt.Printf("Binding address `%s` [syslog]\n", *syslogAddr)
if err = server.ListenUDP(*syslogAddr); err != nil {
fmt.Fprintln(os.Stderr, "argh", err)
os.Exit(1)
}
} }
} }
if err = server.Boot(); err != nil { if err = server.Boot(); err != nil {
@ -89,14 +63,14 @@ func main() {
} }
httpQueryServer := http.Server{Handler: setupHTTP(hub)} httpQueryServer := http.Server{Handler: setupHTTP(hub)}
if *queryAddr == "" { if *querySocket != "" {
fmt.Printf("Binding address `%s` [http]\n", *querySocket) fmt.Printf("Binding address `%s` [http]\n", *querySocket)
unixListener, err := net.Listen("unix", *querySocket) unixListener, err := net.Listen("unix", *querySocket)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Error binding HTTP unix domain socket", err) fmt.Fprintln(os.Stderr, "Error binding HTTP unix domain socket", err)
return return
} }
removeAtExit(*querySocket) defer cleanSocket(*querySocket)
go func() { go func() {
if err := httpQueryServer.Serve(unixListener); err != nil && err != http.ErrServerClosed { if err := httpQueryServer.Serve(unixListener); err != nil && err != http.ErrServerClosed {
fmt.Fprintln(os.Stderr, "error binding", *querySocket, ":", err) fmt.Fprintln(os.Stderr, "error binding", *querySocket, ":", err)
@ -121,7 +95,7 @@ func main() {
fmt.Fprintln(os.Stderr, "Error binding HTTP unix domain socket", err) fmt.Fprintln(os.Stderr, "Error binding HTTP unix domain socket", err)
return return
} }
removeAtExit(*ctlSocket) defer cleanSocket(*ctlSocket)
go func() { go func() {
if err := httpCtlServer.Serve(unixListener); err != nil && err != http.ErrServerClosed { if err := httpCtlServer.Serve(unixListener); err != nil && err != http.ErrServerClosed {
fmt.Fprintln(os.Stderr, "error binding:", err) fmt.Fprintln(os.Stderr, "error binding:", err)
@ -129,17 +103,10 @@ func main() {
}() }()
} }
daemon.SdNotify(false, daemon.SdNotifyReady) // TODO: now we are ready
var wdTick <-chan time.Time
if watchdogTime, err := daemon.SdWatchdogEnabled(false); err == nil && watchdogTime != 0 {
fmt.Println("systemd watchdog enabled")
wdTick = time.Tick(watchdogTime / 2) // much less than systemd default of 30s; TODO: make it configurable
}
for { for {
select { select {
case <-wdTick:
daemon.SdNotify(false, daemon.SdNotifyWatchdog)
case sig := <-interrupt: case sig := <-interrupt:
if sig == syscall.SIGUSR1 { if sig == syscall.SIGUSR1 {
response := make(chan circolog.CommandResponse) response := make(chan circolog.CommandResponse)
@ -163,7 +130,6 @@ func main() {
} }
if sig == syscall.SIGTERM || sig == syscall.SIGINT { if sig == syscall.SIGTERM || sig == syscall.SIGINT {
log.Println("Quitting because of signal", sig) log.Println("Quitting because of signal", sig)
daemon.SdNotify(false, daemon.SdNotifyStopping)
server.Kill() server.Kill()
if err := httpQueryServer.Shutdown(nil); err != nil { if err := httpQueryServer.Shutdown(nil); err != nil {
fmt.Fprintln(os.Stderr, "Error closing http server:", err) fmt.Fprintln(os.Stderr, "Error closing http server:", err)
@ -171,9 +137,6 @@ func main() {
if err := httpCtlServer.Shutdown(nil); err != nil { if err := httpCtlServer.Shutdown(nil); err != nil {
fmt.Fprintln(os.Stderr, "Error closing control server:", err) fmt.Fprintln(os.Stderr, "Error closing control server:", err)
} }
for _, socket := range socketsToRemove {
cleanSocket(socket)
}
return return
} }
} }

View file

@ -1,50 +0,0 @@
package main
import (
"net"
)
// SyslogSocket is a struct eventually containing a net.Listener
// ready with messages, and a Path in case the Listener is not present.
type SyslogSocket struct {
Listener net.Listener
Conn net.PacketConn
Path string
isSocketActivated bool
}
// Set from command-line
func (s *SyslogSocket) Set(v string) error {
err := s.getActivationSocket()
if err == nil && (s.Conn != nil || s.Listener != nil) {
s.isSocketActivated = true
}
if !s.isSocketActivated {
s.Path = v
}
return nil
}
func (s *SyslogSocket) String() string {
if s.isSocketActivated {
return "systemd-provided"
}
return s.Path
}
func (s *SyslogSocket) getActivationSocket() error {
conns, err := PacketConns()
if err == nil && len(conns) > 0 && conns[0] != nil {
s.Conn = conns[0]
return nil
}
listeners, err := Listeners()
if err != nil {
return err
}
if len(listeners) == 0 {
return nil
}
s.Listener = listeners[0]
return nil
}

View file

@ -1,32 +0,0 @@
package data
import "gopkg.in/mcuadros/go-syslog.v2/format"
// Message is currently an alias for format.Logparts, but this is only temporary; sooner or later, a real struct will be used
// The advantage of having an explicit Message is to clear out what data we are sending to circolog "readers"
// This is not necessarily (and not in practice) the same structure that we receive from logging programs
type Message format.LogParts
// LogEntryToMessage converts messages received from writers to the format we promise to readers
func LogEntryToMessage(orig format.LogParts) Message {
m := Message{}
if orig["version"] == 1 { // RFC5424
m["prog"] = orig["app_name"]
m["client"] = orig["client"]
m["host"] = orig["hostname"]
m["proc_id"] = orig["proc_id"]
m["msg"] = orig["message"]
m["facility"] = orig["facility"]
m["time"] = orig["timestamp"]
m["sev"] = orig["severity"]
} else { //RFC3164
m["prog"] = orig["tag"]
m["client"] = orig["client"]
m["host"] = orig["hostname"]
m["msg"] = orig["content"]
m["sev"] = orig["severity"]
m["time"] = orig["timestamp"]
m["proc_id"] = "-"
}
return m
}

View file

@ -1,58 +0,0 @@
package data
import (
"testing"
"time"
"gopkg.in/mcuadros/go-syslog.v2/format"
)
var timeNow = time.Now()
var returnValues = map[string]interface{}{
"prog": "test_app",
"client": "test_client",
"host": "my_machine",
"msg": "test message",
"sev": 3,
"time": timeNow,
}
var msgRFC5424 = format.LogParts{
"version": 1,
"app_name": returnValues["prog"],
"client": returnValues["client"],
"hostname": returnValues["host"],
"proc_id": "spam_process",
"message": returnValues["msg"],
"facility": "hell",
"timestamp": returnValues["time"],
"severity": returnValues["sev"],
}
var msgRFC3164 = format.LogParts{
"tag": returnValues["prog"],
"client": returnValues["client"],
"hostname": returnValues["host"],
"content": returnValues["msg"],
"severity": returnValues["sev"],
"timestamp": returnValues["time"],
"proc_id": "spam_process",
}
var testMessages = []format.LogParts{
msgRFC5424,
msgRFC3164,
}
func TestLogEntryToMessage(t *testing.T) {
for _, msg := range testMessages {
parsedMsg := LogEntryToMessage(msg)
for key, value := range returnValues {
if data, ok := parsedMsg[key]; !ok || data != value {
t.Errorf("Missing/wrong key: %s\nmsg: %s\nparsed msg: %s\n", key, value, data)
}
}
}
}

1
docs/.gitignore vendored
View file

@ -1 +0,0 @@
site

View file

@ -1,20 +0,0 @@
## Why not on github.com
We love collaboration between people, and software development can be
a wonderful playground. Nevertheless, we are not comfortable with the
social-neworkish nature of github. Also, the choice of a platform is something
that is meaningful in itself. Fortunately `go` allows to pull packages from any
git server that is publicly available. Therefore do not be afraid of go-getting
from git.lattuga.net.
lattuga.net is a self-managed server, run with clear principles of not-for-profit and antifascist nature.
We think using situated, decentralized tools and networks is fundamental to get a technological landscape that
doesn't lead to oppression.
### So how to collaborate
Pull request is not the only paradigm for collaboration :)
If you want to collaborate to the software, either drop a mail to
<indirizzo@scamuffo.com>. We are also reachable at [IRC irc.mufhd0.net]

View file

@ -1,61 +0,0 @@
# Circolog
Circolog proposes a different approach to the problem of having useful logs.
Circolog is a syslog server which doesn't write to disk. Ever. It doesn't
consume tons of RAM like elasticsearch/logstash, nor does it give you plots and
a very long history. It is however a very useful tool when you want to minimize
disk writes. We wrote it thinking about user privacy: your logs can be just as
sensitive as your database if you log too much!
[Spiegone su casi d'uso, buttare i log, debuggare rapidamente magari
applicazioni verbose, eccetera]
## Why it is cool in 2 minutes
- It keeps your users safer.
- Read logs conveniently: filter with a proper (and easy) query language!
`grep` is powerful, but sometimes you want something more expressive:
```
circolog-tail -where 'prog=="apache" and msg LIKE "%memory%"'
circolog-tail -where '(prog=="apache" OR prog LIKE "php%") AND msg LIKE "%memory%"'
circolog-tail -where 'prog=="mysql" OR sev >= warning'
```
- Colors: highlight severity and visually group related message
[screenshot]
It's not only about being nice, we swear! Coloring logs also means reaching the
most important entries easily, and grouping related entries together. Output
logs in the format you prefer _now_. Depending on what you're doing, log format
might be useful... or distracting. For example, how many times have you used
the `hostname` part of it? With the common disk-based logging, you need to
choose once and for all how your logs will be saved.
- Hackable: we think that `circolog-tail` is pretty cool, but you definitely can
reuse simpler tools to get logs and filter them the way you prefer. Clients
can read logs using plain HTTP (or websocket). Most of the cool features of
`circolog-tail` are actually implemented server-side, so you can use filters
(or other options) with any client.
- Fast, secure by default (?), easy to deploy. Those are features that you
should expect, not be surprised of! Circolog can easily process thousands
of log entries per seconds, has sane defaults and can be deployed as a single
binary.
## Security considerations
While we try our best not to introduce vulnerabilities, this software is not
meant to be exposed on the wider internet. Beware of binding it on something
different from `localhost`.
Even without being exposed, care must be given to socket permissions: don't let
unprivileged users read your logs! We suggest that you use a dedicated
user/group to run circolog, and make root part of that group.

View file

@ -1,70 +0,0 @@
## Installation and Configuration
If you trust us, just get the compiled binaries and copy them to
`/usr/local/{s,}bin/` (but you shouldn't: [TODO: compiling -> altra pagina]).
Running circolog is pretty easy: just run `circologd` by hand or in your
favourite service manager: here are some contrib scripts for SysV, systemd,
supervisor... but consider them as hints, don't just copy them!
[TODO: add scripts]
### System Integration
While circologd can be your only syslog daemon, you might want to couple it
with another syslog to have the best of both worlds. For example, you could use
rsyslog (or syslog-ng) to write important (ie: `priority >= notice`) log to
persistent storage, letting circolog handle short-term but heavily detailed
logs.
Or you could make circolog get messages from `journald`. While possibilities are
endless, we tried to document some common setups.
#### Use Case: circologd as the only syslog
[mica lo so se funziona, credo di no, ma proviamo] bind it to `/dev/log`.
#### Use Case: circologd receiving messages from syslog-ng
Put this in `/etc/syslog-ng/conf.d/circolog.conf`
```
destination d_circolog {
unix-dgram("/run/circolog/syslog.sock"
flags(syslog-protocol)
);
};
log { source(s_src); destination(d_circolog); };
```
This doesn't tell syslog-ng not to log to disk.
Change `/etc/syslog-ng.conf` according to your need.
#### Use Case: circologd receiving messages from rsyslog
rsyslogd can easily be configured to send every message to circolog:
```
$ModLoad omuxsock
$OMUxSockSocket /tmp/circolog.sock
*.* :omuxsock:
```
If you want to force messages of level `debug` and `info` not to be logged to disk, you can put this snippet
at the **top** of the rules
```
$ModLoad omuxsock
$OMUxSockSocket /tmp/circolog.sock
*.* :omuxsock:
*.=debug;*.=info stop
```
#### Use Case: circologd receiving messages from journald
[TODO: il codice va ancora scritto: bisogna bindare /run/systemd/journal/syslog
con formato != rfc5424]

View file

@ -1,27 +0,0 @@
Query language
==============
Circolog uses a sql-inspired query language. If you know SQL, then you can use
"where clauses" in circolog. If you don't know SQL, don't worry: the language
is easy enough for you to learn the most basic queries without worrying too
much.
You can only filter the rows, you can't sort them or group them in any way.
Reference
---------
Available fields:
- `msg`: the string with the main information
- `prog`: also known as "program" sometimes
- `facility`: an integer describing auth, daemon, user, etc.
- `host`: the hostname where the entry originated
- `time`: date in format `2019-01-07T15:28:58+01:00`
- `sev`: an integer describing severity
Examples
--------
TODO

View file

@ -1,94 +0,0 @@
## A simple start
The bare minimum you need to get circologd on a systemd-based system is this unit.
Other options with more features or more security are provided below
[Unit]
Description=In-memory logging
[Service]
User=root
Group=adm
ExecStart=/usr/local/sbin/circologd -syslogd-socket /run/circolog/syslog.sock -buffer-size 2000 -query-socket /run/circolog/query.sock
[Install]
WantedBy=multi-user.target
## A better unit
This is another unit, which has several security features, such as `DynamicUser`, filesystem restrictions, and
more.
[Unit]
Description=In-memory logging
[Service]
DynamicUser=true
Group=adm
RuntimeDirectory=circolog
# this is important: circologd will respect umask, so if you want to have files that are not world-readable, you must set it
RuntimeDirectoryMode=0750
UMask=0026
ProtectSystem=full
ExecStart=/usr/local/sbin/circologd -syslogd-socket /run/circolog/syslog.sock -buffer-size 2000 -query-socket /run/circolog/query.sock
# security restrictions; useful, but not needed
PrivateTmp=true
PrivateNetwork=true
NoNewPrivileges=true
Restrictnamespaces=true
#optional: watchdog
WatchdogSec=30
[Install]
WantedBy=multi-user.target
## Journald
None of those are integrated with journald, however. The simplest way to integrate with journald is the
following.
First of all, ensure `ForwardToSyslog=yes` in `/etc/systemd/journald.conf`.
Then, you need to run circologd as root and bind it [to a special
address](https://www.freedesktop.org/software/systemd/man/journald.conf.html#Forwarding%20to%20traditional%20syslog%20daemons).
Ok, you don't strictly _need_ to run it as root, but that's the easiest way to run it.
Here is a working unit for this:
[Unit]
Description=In-memory logging
[Service]
User=root
Group=adm
ExecStart=/usr/local/sbin/circologd -syslogd-socket /run/systemd/journal/syslog -buffer-size 2000 -query-socket /run/circolog/query.sock
[Install]
WantedBy=multi-user.target
### journald with socket activation
To run circologd as non-root, while listening on a root-owned socket (`/run/systemd/journal/syslog`) use
socket activation. Create a unit in `/etc/systemd/system/circolog.service`:
[Unit]
Description=In-memory logging
[Service]
User=nobody
Group=nogroup
ExecStart=/usr/local/sbin/circologd -syslogd-socket "" -buffer-size 2000 -query-socket /run/circolog/query.sock
[Install]
WantedBy=multi-user.target
Then symlink the `syslog.service` unit to the newly created one:
ln -sf /etc/systemd/system/circolog.service /etc/systemd/system/syslog.service
and restart the service:
systemctl daemon-reload
systemctl restart syslog.service
Now circolog is activated and receives messages from `journald`.

View file

@ -1,9 +0,0 @@
site_name: Circolog
nav:
- Home: index.md
- Install: install.md
- Queries: query.md
- Hacking: hacking.md
- Systemd: systemd.md
repo_url: https://git.lattuga.net/boyska/circolog
repo_name: 'Repository'

View file

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"git.lattuga.net/boyska/circolog/data"
"github.com/araddon/qlbridge/datasource" "github.com/araddon/qlbridge/datasource"
"github.com/araddon/qlbridge/expr" "github.com/araddon/qlbridge/expr"
"github.com/araddon/qlbridge/value" "github.com/araddon/qlbridge/value"
@ -40,8 +39,7 @@ func (e *ExprValue) Set(value string) error {
return nil return nil
} }
// Validate answers the question whether to include a log line or not. func (e *ExprValue) Validate(line map[string]interface{}) bool {
func (e *ExprValue) Validate(line data.Message) bool {
if e.node == nil { if e.node == nil {
return true return true
} }

View file

@ -3,18 +3,17 @@ package formatter
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"hash/fnv"
"io" "io"
"text/template" "text/template"
"time" "time"
"git.lattuga.net/boyska/circolog/data"
"github.com/mgutz/ansi" "github.com/mgutz/ansi"
"gopkg.in/mcuadros/go-syslog.v2/format"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
// Formatter is an interface, so that multiple implementations can exist // Formatter is an interface, so that multiple implementations can exist
type Formatter func(data.Message) string type Formatter func(format.LogParts) string
var tmplFuncs template.FuncMap var tmplFuncs template.FuncMap
var syslogTmpl *template.Template var syslogTmpl *template.Template
@ -46,21 +45,13 @@ func init() {
return ansi.Color(text, color) // slow; should use colorfunc return ansi.Color(text, color) // slow; should use colorfunc
}, },
"red": ansi.ColorFunc("red+b"), "red": ansi.ColorFunc("red+b"),
"autoColor": func(s string) string {
// from https://weechat.org/blog/post/2011/08/28/Beautify-your-WeeChat
palette := []string{"31", "35", "38", "40", "49", "63", "70", "80", "92", "99", "112", "126", "130", "138", "142", "148", "167", "169", "174", "176", "178", "184", "186", "210", "212", "215", "247"}
hash := fnv.New32()
hash.Write([]byte(s))
picked := palette[int(hash.Sum32())%len(palette)]
return ansi.Color(s, picked)
},
} }
syslogTmpl = template.Must(template.New("syslog").Funcs(tmplFuncs).Parse( syslogTmpl = template.Must(template.New("syslog").Funcs(tmplFuncs).Parse(
"{{color \"yellow\" (rfc822 (index . \"time\")) }} {{index . \"host\"}} " + "{{color \"yellow\" (rfc822 (index . \"timestamp\")) }} {{index . \"hostname\"}} " +
"{{index . \"prog\" | autoColor}}" + "{{index . \"app_name\"}}" +
"{{ if (ne (index . \"proc_id\") \"-\")}}[{{index . \"proc_id\"}}]{{end}}: " + "{{ if (ne (index . \"proc_id\") \"-\")}}[{{index . \"proc_id\"}}]{{end}}: " +
"{{ sevName (index . \"sev\") }} " + "{{ sevName (index . \"severity\") }} " +
"{{index . \"msg\"}}", "{{index . \"message\"}}",
)) ))
} }
@ -87,7 +78,7 @@ func (rf Format) String() string {
return "" return ""
} }
func (rf Format) WriteFormatted(w io.Writer, msg data.Message) error { func (rf Format) WriteFormatted(w io.Writer, msg format.LogParts) error {
return WriteFormatted(w, rf, msg) return WriteFormatted(w, rf, msg)
} }
@ -110,7 +101,7 @@ const (
FormatBSON = iota FormatBSON = iota
) )
func WriteFormatted(w io.Writer, f Format, msg data.Message) error { func WriteFormatted(w io.Writer, f Format, msg format.LogParts) error {
switch f { switch f {
case FormatSyslog: case FormatSyslog:
return syslogTmpl.Execute(w, msg) return syslogTmpl.Execute(w, msg)

View file

@ -1,49 +0,0 @@
package formatter
import (
"errors"
syslog "gopkg.in/mcuadros/go-syslog.v2"
"gopkg.in/mcuadros/go-syslog.v2/format"
)
// SyslogRFC is the formatter that the server should use
type SyslogRFC struct{ format.Format }
func (rfc *SyslogRFC) Set(v string) error {
newval, err := parseRFCValue(v)
if err != nil {
return err
}
rfc.Format = newval
return nil
}
func (rfc *SyslogRFC) String() string {
switch {
case rfc.Format == syslog.Automatic:
return "auto"
case rfc.Format == syslog.RFC3164:
return "rfc3164"
case rfc.Format == syslog.RFC5424:
return "rfc5424"
}
return ""
}
func parseRFCValue(v string) (format.Format, error) {
switch {
case v == "rfc3164":
return syslog.RFC3164, nil
case v == "rfc5424":
return syslog.RFC5424, nil
case v == "auto":
return syslog.Automatic, nil
default:
return nil, ErrRFCNotSupported
}
}
// ErrRFCNotSupported is raised if the supplied rfc string is
// not recognized.
var ErrRFCNotSupported = errors.New("RFC not known")

13
hub.go
View file

@ -6,7 +6,6 @@ import (
"os" "os"
"time" "time"
"git.lattuga.net/boyska/circolog/data"
"git.lattuga.net/boyska/circolog/filtering" "git.lattuga.net/boyska/circolog/filtering"
"gopkg.in/mcuadros/go-syslog.v2/format" "gopkg.in/mcuadros/go-syslog.v2/format"
) )
@ -14,11 +13,10 @@ import (
// Client represent a client connected via websocket. Its most important field is the messages channel, where // Client represent a client connected via websocket. Its most important field is the messages channel, where
// new messages are sent. // new messages are sent.
type Client struct { type Client struct {
Messages chan data.Message // only hub should write/close this Messages chan format.LogParts // only hub should write/close this
Options ClientOptions Options ClientOptions
} }
// ClientOptions is a struct containing connection options for every reader
type ClientOptions struct { type ClientOptions struct {
BacklogLength int // how many past messages the client wants to receive upon connection BacklogLength int // how many past messages the client wants to receive upon connection
Nofollow bool // if Nofollow is true, the hub will not keep this client permanently. Rather, it will send every message to "Messages" and close the channel. Use this if you want to get the messages one-shot Nofollow bool // if Nofollow is true, the hub will not keep this client permanently. Rather, it will send every message to "Messages" and close the channel. Use this if you want to get the messages one-shot
@ -102,7 +100,7 @@ func (h *Hub) register(cl Client) {
item := buf.Value item := buf.Value
if item != nil { if item != nil {
select { // send with short timeout select { // send with short timeout
case cl.Messages <- item.(data.Message): case cl.Messages <- item.(format.LogParts):
break break
case <-time.After(500 * time.Millisecond): case <-time.After(500 * time.Millisecond):
close(cl.Messages) close(cl.Messages)
@ -134,13 +132,12 @@ func (h *Hub) Run() {
delete(h.clients, cl) delete(h.clients, cl)
} }
case msg := <-h.LogMessages: case msg := <-h.LogMessages:
newmsg := data.LogEntryToMessage(msg) if active == true && filter.Validate(msg) {
if active == true && filter.Validate(newmsg) { h.circbuf.Value = msg
h.circbuf.Value = newmsg
h.circbuf = h.circbuf.Next() h.circbuf = h.circbuf.Next()
for client := range h.clients { for client := range h.clients {
select { // send without blocking select { // send without blocking
case client.Messages <- newmsg: case client.Messages <- msg:
break break
default: default:
break break

View file

@ -3,12 +3,11 @@ package circolog
import ( import (
"testing" "testing"
"git.lattuga.net/boyska/circolog/data"
"gopkg.in/mcuadros/go-syslog.v2/format" "gopkg.in/mcuadros/go-syslog.v2/format"
) )
func msg(s string) format.LogParts { func msg(s string) format.LogParts {
return format.LogParts{"content": s} return format.LogParts{"text": s}
} }
func hubCount(h Hub) int { func hubCount(h Hub) int {
@ -20,10 +19,10 @@ var DefaultClient ClientOptions = ClientOptions{Nofollow: true, BacklogLength: -
func hubToArrayOpt(h Hub, opt ClientOptions) []string { func hubToArrayOpt(h Hub, opt ClientOptions) []string {
r := make([]string, 0) r := make([]string, 0)
cl := Client{Options: opt} cl := Client{Options: opt}
cl.Messages = make(chan data.Message) cl.Messages = make(chan format.LogParts)
h.Register <- cl h.Register <- cl
for m := range cl.Messages { for m := range cl.Messages {
r = append(r, m["msg"].(string)) r = append(r, m["text"].(string))
} }
return r return r
} }

View file

@ -1,20 +0,0 @@
#!/bin/bash
set -u
for goosarch in $(go tool dist list | grep -vw -e aix -e js/wasm -e plan9 -e solaris -e android -e nacl)
do
mkdir -p "build/$goosarch"
goos=$(cut -d/ -f 1 <<<$goosarch)
goarch=$(cut -d/ -f 2 <<<$goosarch)
for cmd in cmd/*; do
GOOS=${goos} GOARCH=${goarch} go build -o "build/$goos/$goarch/$(basename $cmd)" ./$cmd
done
done
find build/ -type f|cut -d/ -f 1-3|uniq|while read -r dir; do
find $dir/ -type f -executable | xargs sha1sum > $dir/SHA1SUMS.txt
# TODO: touch to last commit date maybe
find build -exec touch -d @1234567890 {} \;
zip -q -X -j -r "circolog-$(git describe --tags --always)-$(cut -d/ -f 2-3 <<<"$dir"|tr / -)" "$dir"
done

@ -1 +0,0 @@
Subproject commit 166aad3f993ce4a67bf486e62d637c834c8a8fe6