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
|
* `%q`: query
|
||||||
* `%a`: application name
|
* `%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
|
# License
|
||||||
`pgterminate` is released under [The Unlicense](LICENSE) license. Code is under public domain.
|
`pgterminate` is released under [The Unlicense](LICENSE) license. Code is under public domain.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session represents a PostgreSQL backend
|
// Session represents a PostgreSQL backend
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Pid int64
|
Pid int64 `json:"pid"`
|
||||||
User string
|
User string `json:"user"`
|
||||||
Db string
|
Db string `json:"database"`
|
||||||
Client string
|
Client string `json:"client"`
|
||||||
State string
|
State string `json:"state"`
|
||||||
Query string
|
Query string `json:"query"`
|
||||||
StateDuration float64
|
StateDuration float64 `json:"state_duration"`
|
||||||
ApplicationName string
|
ApplicationName string `json:"application_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSession instanciates a Session
|
// NewSession instanciates a Session
|
||||||
|
|
@ -53,6 +54,12 @@ func (s *Session) Format(format string) string {
|
||||||
return output
|
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
|
// IsIdle returns true when a session is doing nothing
|
||||||
func (s *Session) IsIdle() bool {
|
func (s *Session) IsIdle() bool {
|
||||||
if s.State == "idle" || s.State == "idle in transaction" || s.State == "idle in transaction (aborted)" {
|
if s.State == "idle" || s.State == "idle in transaction" || s.State == "idle in transaction (aborted)" {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,32 @@
|
||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"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) {
|
func TestSessionEqual(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
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.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.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.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.PidFile, "pid-file", "", "Write process id into a file")
|
||||||
flag.StringVar(&config.SyslogIdent, "syslog-ident", "pgterminate", "Define syslog tag")
|
flag.StringVar(&config.SyslogIdent, "syslog-ident", "pgterminate", "Define syslog tag")
|
||||||
flag.StringVar(&config.SyslogFacility, "syslog-facility", "", "Define syslog facility from LOCAL0 to LOCAL7")
|
flag.StringVar(&config.SyslogFacility, "syslog-facility", "", "Define syslog facility from LOCAL0 to LOCAL7")
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#active-timeout: 10
|
#active-timeout: 10
|
||||||
#log-file: /var/log/pgterminate/pgterminate.log
|
#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: '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
|
#pid-file: /var/run/pgterminate/pgterminate.pid
|
||||||
#log-destination: console|file|syslog
|
#log-destination: console|file|syslog
|
||||||
#syslog-ident: pgterminate
|
#syslog-ident: pgterminate
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ func NewConsole(format string, sessions chan *base.Session) Notifier {
|
||||||
func (c *Console) Run() {
|
func (c *Console) Run() {
|
||||||
log.Info("Starting console notifier")
|
log.Info("Starting console notifier")
|
||||||
for session := range c.sessions {
|
for session := range c.sessions {
|
||||||
log.Info(session.Format(c.format))
|
if c.format == "json" {
|
||||||
|
log.Info(session.JSON())
|
||||||
|
} else {
|
||||||
|
log.Info(session.Format(c.format))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package notifier
|
package notifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -19,7 +20,7 @@ type File struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFile creates a file notifier
|
// 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{
|
return &File{
|
||||||
name: name,
|
name: name,
|
||||||
format: format,
|
format: format,
|
||||||
|
|
@ -35,11 +36,27 @@ func (f *File) Run() {
|
||||||
|
|
||||||
for session := range f.sessions {
|
for session := range f.sessions {
|
||||||
timestamp := time.Now().Format(time.RFC3339)
|
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)
|
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
|
// open opens a log file
|
||||||
func (f *File) open() {
|
func (f *File) open() {
|
||||||
var err error
|
var err error
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ func (s *Syslog) Run() {
|
||||||
base.Panic(err)
|
base.Panic(err)
|
||||||
}
|
}
|
||||||
for session := range s.sessions {
|
for session := range s.sessions {
|
||||||
s.writer.Info(session.Format(s.format))
|
if s.format == "json" {
|
||||||
|
s.writer.Info(session.JSON())
|
||||||
|
} else {
|
||||||
|
s.writer.Info(session.Format(s.format))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue