Repository: rs/zerolog
Pull request #262 (2020-10-09 18:45:19)

Add syslog CEE support
This patch adds a `SyslogCEEWriter` which is like `SyslogLeveledWriter`, but also adds a [MITRE CEE](http://cee.mitre.org/language/1.0-beta1/clt.html#appendix-1-cee-over-syslog-transport-mapping) marker to better support JSON log parsing in [rsyslog](https://www.rsyslog.com/json-elasticsearch/) and [syslog-ng](https://support.oneidentity.com/technical-documents/syslog-ng-open-source-edition/3.24/administration-guide/71#TOPIC-1298138).

This implementation includes a string append. Trying to avoid any allocations would add significant complexity, and I figure a string append is likely insignificant compared to the inherent overhead of syslog.


Patch:
diff --git a/syslog.go b/syslog.go
index ef3b2c8..c408283 100644
--- a/syslog.go
+++ b/syslog.go
@@ -7,6 +7,10 @@ import (
 	"io"
 )
 
+// See http://cee.mitre.org/language/1.0-beta1/clt.html#syslog
+// or https://www.rsyslog.com/json-elasticsearch/
+const ceePrefix = "@cee:"
+
 // SyslogWriter is an interface matching a syslog.Writer struct.
 type SyslogWriter interface {
 	io.Writer
@@ -19,17 +23,34 @@ type SyslogWriter interface {
 }
 
 type syslogWriter struct {
-	w SyslogWriter
+	w      SyslogWriter
+	prefix string
 }
 
 // SyslogLevelWriter wraps a SyslogWriter and call the right syslog level
 // method matching the zerolog level.
 func SyslogLevelWriter(w SyslogWriter) LevelWriter {
-	return syslogWriter{w}
+	return syslogWriter{w, ""}
+}
+
+// SyslogCEEWriter wraps a SyslogWriter with a SyslogLevelWriter that adds a
+// MITRE CEE prefix for JSON syslog entries, compatible with rsyslog 
+// and syslog-ng JSON logging support. 
+// See https://www.rsyslog.com/json-elasticsearch/
+func SyslogCEEWriter(w SyslogWriter) LevelWriter {
+	return syslogWriter{w, ceePrefix}
 }
 
 func (sw syslogWriter) Write(p []byte) (n int, err error) {
-	return sw.w.Write(p)
+	var pn int
+	if sw.prefix != "" {
+		pn, err = sw.w.Write([]byte(sw.prefix))
+		if err != nil {
+			return pn, err
+		}
+	}
+	n, err = sw.w.Write(p)
+	return pn + n, err
 }
 
 // WriteLevel implements LevelWriter interface.
@@ -37,22 +58,23 @@ func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) {
 	switch level {
 	case TraceLevel:
 	case DebugLevel:
-		err = sw.w.Debug(string(p))
+		err = sw.w.Debug(sw.prefix + string(p))
 	case InfoLevel:
-		err = sw.w.Info(string(p))
+		err = sw.w.Info(sw.prefix + string(p))
 	case WarnLevel:
-		err = sw.w.Warning(string(p))
+		err = sw.w.Warning(sw.prefix + string(p))
 	case ErrorLevel:
-		err = sw.w.Err(string(p))
+		err = sw.w.Err(sw.prefix + string(p))
 	case FatalLevel:
-		err = sw.w.Emerg(string(p))
+		err = sw.w.Emerg(sw.prefix + string(p))
 	case PanicLevel:
-		err = sw.w.Crit(string(p))
+		err = sw.w.Crit(sw.prefix + string(p))
 	case NoLevel:
-		err = sw.w.Info(string(p))
+		err = sw.w.Info(sw.prefix + string(p))
 	default:
 		panic("invalid level")
 	}
+	// Any CEE prefix is not part of the message, so we don't include its length
 	n = len(p)
 	return
 }


Test patch:
diff --git a/syslog_test.go b/syslog_test.go
index 927a9c3..c168ba6 100644
--- a/syslog_test.go
+++ b/syslog_test.go
@@ -4,7 +4,9 @@
 package zerolog
 
 import (
+	"bytes"
 	"reflect"
+	"strings"
 	"testing"
 )
 
@@ -68,3 +70,39 @@ func TestSyslogWriter(t *testing.T) {
 		t.Errorf("Invalid syslog message routing: want %v, got %v", want, got)
 	}
 }
+
+type testCEEwriter struct {
+	buf *bytes.Buffer
+}
+
+// Only implement one method as we're just testing the prefixing
+func (c testCEEwriter) Debug(m string) error { return nil }
+
+func (c testCEEwriter) Info(m string) error {
+	_, err := c.buf.Write([]byte(m))
+	return err
+}
+
+func (c testCEEwriter) Warning(m string) error { return nil }
+
+func (c testCEEwriter) Err(m string) error { return nil }
+
+func (c testCEEwriter) Emerg(m string) error { return nil }
+
+func (c testCEEwriter) Crit(m string) error { return nil }
+
+func (c testCEEwriter) Write(b []byte) (int, error) {
+	return c.buf.Write(b)
+}
+
+func TestSyslogWriter_WithCEE(t *testing.T) {
+	var buf bytes.Buffer
+	sw := testCEEwriter{&buf}
+	log := New(SyslogCEEWriter(sw))
+	log.Info().Str("key", "value").Msg("message string")
+	got := string(buf.Bytes())
+	want := "@cee:{"
+	if !strings.HasPrefix(got, want) {
+		t.Errorf("Bad CEE message start: want %v, got %v", want, got)
+	}
+}