feat: Add JSON logging
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
18cbc70495
commit
e72feaf0f6
8 changed files with 77 additions and 13 deletions
|
|
@ -131,5 +131,13 @@ The following placeholders are available to format log messages using `log-forma
|
|||
* `%q`: query
|
||||
* `%a`: application name
|
||||
|
||||
Set `log-format` to `json` to log each session as a JSON object instead:
|
||||
|
||||
```
|
||||
pgterminate -log-format json
|
||||
```
|
||||
|
||||
Keys are `pid`, `user`, `database`, `client`, `state`, `query`, `state_duration` and `application_name`. With `log-destination: file`, a `timestamp` key is added to each object.
|
||||
|
||||
# License
|
||||
`pgterminate` is released under [The Unlicense](LICENSE) license. Code is under public domain.
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Session represents a PostgreSQL backend
|
||||
type Session struct {
|
||||
Pid int64
|
||||
User string
|
||||
Db string
|
||||
Client string
|
||||
State string
|
||||
Query string
|
||||
StateDuration float64
|
||||
ApplicationName string
|
||||
Pid int64 `json:"pid"`
|
||||
User string `json:"user"`
|
||||
Db string `json:"database"`
|
||||
Client string `json:"client"`
|
||||
State string `json:"state"`
|
||||
Query string `json:"query"`
|
||||
StateDuration float64 `json:"state_duration"`
|
||||
ApplicationName string `json:"application_name"`
|
||||
}
|
||||
|
||||
// NewSession instanciates a Session
|
||||
|
|
@ -53,6 +54,12 @@ func (s *Session) Format(format string) string {
|
|||
return output
|
||||
}
|
||||
|
||||
// JSON returns a Session marshaled as a JSON string
|
||||
func (s *Session) JSON() string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// IsIdle returns true when a session is doing nothing
|
||||
func (s *Session) IsIdle() bool {
|
||||
if s.State == "idle" || s.State == "idle in transaction" || s.State == "idle in transaction (aborted)" {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,32 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSessionJSON(t *testing.T) {
|
||||
session := &Session{
|
||||
Pid: 1,
|
||||
User: "test",
|
||||
Db: "db",
|
||||
Client: "127.0.0.1:5432",
|
||||
State: "active",
|
||||
Query: "select 1",
|
||||
StateDuration: 1.5,
|
||||
ApplicationName: "psql",
|
||||
}
|
||||
|
||||
var got Session
|
||||
if err := json.Unmarshal([]byte(session.JSON()), &got); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
if got != *session {
|
||||
t.Errorf("got %+v; want %+v", got, *session)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func main() {
|
|||
flag.Float64Var(&config.ActiveTimeout, "active-timeout", 0, "Time for active connections to be terminated in seconds")
|
||||
flag.StringVar(&config.LogDestination, "log-destination", "console", "Log destination between 'console', 'syslog' or 'file'")
|
||||
flag.StringVar(&config.LogFile, "log-file", "", "Write logs to a file")
|
||||
flag.StringVar(&config.LogFormat, "log-format", "pid=%p user=%u db=%d client=%r state=%s state_duration=%m query=%q", "Represent messages using this format")
|
||||
flag.StringVar(&config.LogFormat, "log-format", "pid=%p user=%u db=%d client=%r state=%s state_duration=%m query=%q", "Represent messages using this format, or 'json' to log sessions as JSON")
|
||||
flag.StringVar(&config.PidFile, "pid-file", "", "Write process id into a file")
|
||||
flag.StringVar(&config.SyslogIdent, "syslog-ident", "pgterminate", "Define syslog tag")
|
||||
flag.StringVar(&config.SyslogFacility, "syslog-facility", "", "Define syslog facility from LOCAL0 to LOCAL7")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#active-timeout: 10
|
||||
#log-file: /var/log/pgterminate/pgterminate.log
|
||||
#log-format: 'pid=%p user=%u db=%d client=%r state=%s state_duration=%m query=%q'
|
||||
#log-format: json
|
||||
#pid-file: /var/run/pgterminate/pgterminate.pid
|
||||
#log-destination: console|file|syslog
|
||||
#syslog-ident: pgterminate
|
||||
|
|
|
|||
|
|
@ -23,9 +23,13 @@ func NewConsole(format string, sessions chan *base.Session) Notifier {
|
|||
func (c *Console) Run() {
|
||||
log.Info("Starting console notifier")
|
||||
for session := range c.sessions {
|
||||
if c.format == "json" {
|
||||
log.Info(session.JSON())
|
||||
} else {
|
||||
log.Info(session.Format(c.format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload for handling SIGHUP signals
|
||||
func (c *Console) Reload() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -19,7 +20,7 @@ type File struct {
|
|||
}
|
||||
|
||||
// NewFile creates a file notifier
|
||||
func NewFile(format string, name string, sessions chan *base.Session) Notifier {
|
||||
func NewFile(name string, format string, sessions chan *base.Session) Notifier {
|
||||
return &File{
|
||||
name: name,
|
||||
format: format,
|
||||
|
|
@ -35,11 +36,27 @@ func (f *File) Run() {
|
|||
|
||||
for session := range f.sessions {
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
_, err := f.handle.WriteString(timestamp + " " + session.Format(f.format) + "\n")
|
||||
var line string
|
||||
if f.format == "json" {
|
||||
line = jsonLine(timestamp, session)
|
||||
} else {
|
||||
line = timestamp + " " + session.Format(f.format)
|
||||
}
|
||||
_, err := f.handle.WriteString(line + "\n")
|
||||
base.Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonLine marshals a session as a self-contained JSON object including the
|
||||
// timestamp, so each file line stays valid JSON instead of being prefixed
|
||||
func jsonLine(timestamp string, session *base.Session) string {
|
||||
b, _ := json.Marshal(struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
*base.Session
|
||||
}{timestamp, session})
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// open opens a log file
|
||||
func (f *File) open() {
|
||||
var err error
|
||||
|
|
|
|||
|
|
@ -53,9 +53,13 @@ func (s *Syslog) Run() {
|
|||
base.Panic(err)
|
||||
}
|
||||
for session := range s.sessions {
|
||||
if s.format == "json" {
|
||||
s.writer.Info(session.JSON())
|
||||
} else {
|
||||
s.writer.Info(session.Format(s.format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload disconnect from syslog daemon and re-connect
|
||||
// Executed when receiving SIGHUP signal
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue