From c3799c57924a9004f509de808f0311e301ec75a0 Mon Sep 17 00:00:00 2001 From: Lukasz Wojciechowski Date: Mon, 30 Jul 2018 16:51:04 +0200 Subject: Implement Serializer for Text SerializerText converts Entry structure into text format stored in raw byte buffer. There are several customizations that can be made by changing fields of the SerializerText structure: TimestampMode - defines the way timestamp is serialized. There are 3 options: TimestampModeNone - no timestamp is used; TimestampModeDiff (default) - relative time in seconds with microsecond precision since creation of SerializerText object; TimestampModeFull - absoltue time in customizable format. TimeFormat - used only in case of TimestampModeFull defines format for serializing Entry's timestamp. It can be set to one of the values accepted by the time.Format. The default value is time.RFC3339. UseColors (default true) - defines if logs should be colorized. If set to true, then log level and properties keys are serialized with predefined colors. QuoteMode - defines if log message, keys and values of the properties should be quoted before serialization. There are 4 available options: QuoteModeNone - no quoting is used; QuoteModeSpecial - any string containing at least one special character is quoted; QuoteModeSpecialAndEmpty - any empty string or one that contains at least one special character is quoted; QuoteModeAll - all strings are quoted. Common characters are: 'a'-'z', 'A'-'Z', '0'-'9', '-', '.', '_', '/', '@', '^', '+' All other characters are special. Change-Id: Ia861c32bf0cf9bc30b1c42e06d9142f38f78899a Signed-off-by: Lukasz Wojciechowski --- logger/error.go | 3 + logger/serializer_text.go | 300 ++++++++++++++++++++++++++ logger/serializer_text_test.go | 469 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 772 insertions(+) create mode 100644 logger/serializer_text.go create mode 100644 logger/serializer_text_test.go diff --git a/logger/error.go b/logger/error.go index df841a7..0a0b0ba 100644 --- a/logger/error.go +++ b/logger/error.go @@ -24,4 +24,7 @@ var ( // ErrInvalidBackendName is returned in case of unknown backend name. ErrInvalidBackendName = errors.New("invalid backend name") + + // ErrInvalidEntry is returned in case of invalid entry struct. + ErrInvalidEntry = errors.New("invalid log entry structure") ) diff --git a/logger/serializer_text.go b/logger/serializer_text.go new file mode 100644 index 0000000..80e04fc --- /dev/null +++ b/logger/serializer_text.go @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package logger + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "sync" + "time" +) + +// TimestampMode defines possible time stamp logging modes. +type TimestampMode uint8 + +const ( + // TimestampModeNone - no time stamp used. + TimestampModeNone TimestampMode = iota + // TimestampModeDiff - seconds since creation of SerializerText. + // Usualy it is equal to the binary start as logger and its serializer + // are created mostly by packages' init functions. + TimestampModeDiff + // TimestampModeFull - date and time in UTC. + TimestampModeFull +) + +// QuoteMode defines possible quoting modes. +type QuoteMode uint8 + +const ( + // QuoteModeNone - no quoting is used. + QuoteModeNone QuoteMode = iota + // QuoteModeSpecial - values containing special characters are quoted. + QuoteModeSpecial + // QuoteModeSpecialAndEmpty - values containing special characters and empty are quoted. + QuoteModeSpecialAndEmpty + // QuoteModeAll - all values are quoted. + QuoteModeAll +) + +// Define default SerializerText properties. +const ( + // DefaultSerializerTextTimeFormat is the default date and time format. + DefaultSerializerTextTimeFormat = time.RFC3339 + // DefaultTimestampMode is the default mode for logging time stamp. + DefaultTimestampMode = TimestampModeDiff + // DefaultQuoteMode is the default quoting mode. + DefaultQuoteMode = QuoteModeSpecialAndEmpty +) + +// Define termninal codes for colors. +const ( + red = "\x1b[31m" + green = "\x1b[32m" + yellow = "\x1b[33m" + blue = "\x1b[34m" + magenta = "\x1b[35m" + cyan = "\x1b[36m" + + bold = "\x1b[1m" + invert = "\x1b[7m" + off = "\x1b[0m" + + propkey = cyan +) + +// levelColoring stores mapping between logger levels and their colors. +var levelColoring = map[Level]string{ + EmergLevel: red + bold + invert, + AlertLevel: cyan + bold + invert, + CritLevel: magenta + bold + invert, + ErrLevel: red + bold, + WarningLevel: yellow + bold, + NoticeLevel: blue + bold, + InfoLevel: green + bold, + DebugLevel: bold, +} + +// asciiTableSize defines SerializerText.ascii tab size. +const asciiTableSize = 128 + +// SerializerText serializes entry to text format. +type SerializerText struct { + // TimeFormat defines format for displaying date and time. + // Used only when TimestampMode is set to TimestampModeFull. + // See https://godoc.org/time#Time.Format description for details. + TimeFormat string + + // TimestampMode defines mode for logging date and time. + TimestampMode TimestampMode + + // QuoteMode defines which values are quoted. + QuoteMode QuoteMode + + // UseColors set to true enables usage of colors. + UseColors bool + + // ascii defines which characters are special and need quoting. + ascii [asciiTableSize]bool + + // initASCII ensures that initialization of ascii is done only once. + initASCII sync.Once + + // baseTime is the Timestamp from which elapsed time is calculated in TimestampModeDiff. + baseTime time.Time +} + +// NewSerializerText creates and returns a new default SerializerText with default values. +func NewSerializerText() *SerializerText { + return &SerializerText{ + TimestampMode: DefaultTimestampMode, + TimeFormat: DefaultSerializerTextTimeFormat, + UseColors: true, + QuoteMode: DefaultQuoteMode, + baseTime: time.Now(), + } +} + +// setDefaultsOnInvalid fixes invalid serializer's fields to default values. +func (s *SerializerText) setDefaultsOnInvalid() { + if s.TimestampMode > TimestampModeFull { + s.TimestampMode = DefaultTimestampMode + } + if len(s.TimeFormat) == 0 { + s.TimeFormat = DefaultSerializerTextTimeFormat + } + if s.QuoteMode > QuoteModeAll { + s.QuoteMode = DefaultQuoteMode + } + if s.baseTime.IsZero() { + s.baseTime = time.Now() + } +} + +// initASCIITable initializes ascii table. +func (s *SerializerText) initASCIITable() { + for i := 'a'; i <= 'z'; i++ { + s.ascii[i] = true + } + for i := 'A'; i <= 'Z'; i++ { + s.ascii[i] = true + } + for i := '0'; i <= '9'; i++ { + s.ascii[i] = true + } + for _, i := range []byte{'-', '.', '_', '/', '@', '^', '+'} { + s.ascii[i] = true + } +} + +// quotingFormat returns formatting string for message depending on quoting settings. +func (s *SerializerText) quotingFormat(msg string) string { + const ( + quoting = "%q" + notquoting = "%s" + ) + + switch s.QuoteMode { + case QuoteModeSpecialAndEmpty: + if len(msg) == 0 { + return quoting + } + fallthrough + case QuoteModeSpecial: + s.initASCII.Do(s.initASCIITable) + for _, i := range msg { + if i >= asciiTableSize || !s.ascii[i] { + return quoting + } + } + return notquoting + case QuoteModeAll: + return quoting + case QuoteModeNone: + default: + } + return notquoting +} + +// appendTimestamp to log message being created in buf. +func (s *SerializerText) appendTimestamp(buf io.Writer, t *time.Time) (err error) { + switch s.TimestampMode { + case TimestampModeDiff: + const precision = 6 + _, err = fmt.Fprintf(buf, "[%.*f] ", precision, t.Sub(s.baseTime).Seconds()) + case TimestampModeFull: + _, err = fmt.Fprintf(buf, "[%s] ", t.Format(s.TimeFormat)) + } + return err +} + +// appendLevel to log message being created in buf. +func (s *SerializerText) appendLevel(buf io.Writer, level Level) (err error) { + var format string + if s.UseColors { + format = "[" + levelColoring[level] + "%.*s" + off + "] " + } else { + format = "[%.*s] " + } + const precision = 3 + _, err = fmt.Fprintf(buf, format, precision, strings.ToUpper(level.String())) + return err +} + +// appendMessage to log message being created in buf. +func (s *SerializerText) appendMessage(buf io.Writer, msg string) (err error) { + format := s.quotingFormat(msg) + " " + _, err = fmt.Fprintf(buf, format, msg) + return err +} + +// appendProperties to log message being created in buf. +func (s *SerializerText) appendProperties(buf io.Writer, properties Properties) (err error) { + if len(properties) == 0 { + return + } + _, err = fmt.Fprintf(buf, "{") + if err != nil { + return err + } + keys := make([]string, len(properties)) + i := 0 + for k := range properties { + keys[i] = k + i++ + } + sort.Strings(keys) + + for _, k := range keys { + v := properties[k] + format := s.quotingFormat(k) + if s.UseColors { + format = propkey + format + off + } + format = format + ":" + _, err = fmt.Fprintf(buf, format, k) + if err != nil { + return err + } + + value := fmt.Sprint(v) + format = s.quotingFormat(value) + ";" + _, err = fmt.Fprintf(buf, format, value) + if err != nil { + return err + } + } + _, err = fmt.Fprintf(buf, "}") + return err +} + +// serialize writes parts of Entry to given writer. +func (s *SerializerText) serialize(entry *Entry, buf io.Writer) error { + if entry == nil { + return ErrInvalidEntry + } + err := s.appendTimestamp(buf, &entry.Timestamp) + if err != nil { + return err + } + err = s.appendLevel(buf, entry.Level) + if err != nil { + return err + } + err = s.appendMessage(buf, entry.Message) + if err != nil { + return err + } + err = s.appendProperties(buf, entry.Properties) + return err +} + +// Serialize implements Serializer interface in SerializerText. +func (s *SerializerText) Serialize(entry *Entry) ([]byte, error) { + s.setDefaultsOnInvalid() + + buf := &bytes.Buffer{} + err := s.serialize(entry, buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/logger/serializer_text_test.go b/logger/serializer_text_test.go new file mode 100644 index 0000000..d0ea329 --- /dev/null +++ b/logger/serializer_text_test.go @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package logger + +import ( + "bytes" + "errors" + "io" + "strings" + "time" + + . "github.com/onsi/ginkgo" + T "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +// FailingWriter is a dummy io.Writer implementation that returns a given error after writting +// given amount of bytes. +type FailingWriter struct { + failAfter int + fail error +} + +func NewFailingWriter(failAfter int, fail error) io.Writer { + return &FailingWriter{ + failAfter: failAfter, + fail: fail, + } +} +func (w *FailingWriter) Write(p []byte) (n int, err error) { + l := len(p) + if l < w.failAfter { + w.failAfter -= l + return l, nil + } + return l - w.failAfter, w.fail +} + +var _ = Describe("SerializerText", func() { + const ( + empty = "" + nospecial = "Alice_has_got_7_cats." + special = "#$?" + mixed = "What?" + ) + + var ( + s *SerializerText + before time.Time + testError error + ) + + BeforeEach(func() { + before = time.Now() + s = NewSerializerText() + testError = errors.New("TestError") + }) + Describe("NewSerializerText", func() { + It("should create a new object with default configuration", func() { + Expect(s).NotTo(BeNil()) + Expect(s.TimestampMode).To(Equal(DefaultTimestampMode)) + Expect(s.TimeFormat).To(Equal(DefaultSerializerTextTimeFormat)) + Expect(s.UseColors).To(BeTrue()) + Expect(s.QuoteMode).To(Equal(DefaultQuoteMode)) + Expect(s.baseTime).To(BeTemporally(">", before)) + Expect(s.baseTime).To(BeTemporally("<", time.Now())) + Expect(s.ascii).To(HaveLen(asciiTableSize)) + }) + }) + Describe("setDefaultsOnInvalid", func() { + It("should set empty object to correct values", func() { + before = time.Now() + s = &SerializerText{} + s.setDefaultsOnInvalid() + + Expect(s).NotTo(BeNil()) + Expect(s.TimeFormat).To(Equal(DefaultSerializerTextTimeFormat)) + Expect(s.baseTime).To(BeTemporally(">", before)) + Expect(s.baseTime).To(BeTemporally("<", time.Now())) + Expect(s.ascii).To(HaveLen(asciiTableSize)) + }) + It("should set timestamp mode to default value if out of range", func() { + s = &SerializerText{} + s.TimestampMode = 0xBB + s.setDefaultsOnInvalid() + + Expect(s.TimestampMode).To(Equal(DefaultTimestampMode)) + }) + It("should set time formatter mode to default value if empty", func() { + s = &SerializerText{} + s.TimeFormat = "" + s.setDefaultsOnInvalid() + + Expect(s.TimeFormat).To(Equal(DefaultSerializerTextTimeFormat)) + }) + It("should set quote mode to default value if out of range", func() { + s = &SerializerText{} + s.QuoteMode = 0xBB + s.setDefaultsOnInvalid() + + Expect(s.QuoteMode).To(Equal(DefaultQuoteMode)) + }) + It("should set base time to now if not set before", func() { + s = &SerializerText{} + s.baseTime = time.Time{} + before = time.Now() + s.setDefaultsOnInvalid() + + Expect(s.baseTime).To(BeTemporally(">", before)) + Expect(s.baseTime).To(BeTemporally("<", time.Now())) + }) + T.DescribeTable("should not change timestamp mode if valid", + func(mode TimestampMode) { + s = &SerializerText{} + s.TimestampMode = mode + s.setDefaultsOnInvalid() + + Expect(s.TimestampMode).To(Equal(mode)) + }, + T.Entry("TimestampModeNone", TimestampModeNone), + T.Entry("TimestampModeDiff", TimestampModeDiff), + T.Entry("TimestampModeFull", TimestampModeFull), + ) + It("should not change time format if set", func() { + s = &SerializerText{} + s.TimeFormat = time.ANSIC + s.setDefaultsOnInvalid() + + Expect(s.TimeFormat).To(Equal(time.ANSIC)) + }) + T.DescribeTable("should not change quote mode if valid", + func(mode QuoteMode) { + s = &SerializerText{} + s.QuoteMode = mode + s.setDefaultsOnInvalid() + + Expect(s.QuoteMode).To(Equal(mode)) + }, + T.Entry("QuoteModeNone", QuoteModeNone), + T.Entry("QuoteModeSpecial", QuoteModeSpecial), + T.Entry("QuoteModeSpecialAndEmpty", QuoteModeSpecialAndEmpty), + T.Entry("QuoteModeAll", QuoteModeAll), + ) + It("should not change base time if set", func() { + magicTime := time.Unix(1234567890, 0) + s = &SerializerText{} + s.baseTime = magicTime + s.setDefaultsOnInvalid() + + Expect(s.baseTime).To(BeTemporally("==", magicTime)) + }) + }) + Describe("initASCIITable", func() { + const good = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789" + + "-._/@^+" + It("should init table properly", func() { + // Clear the table. + for i := 0; i < asciiTableSize; i++ { + s.ascii[i] = false + } + + s.initASCIITable() + for i := 0; i < asciiTableSize; i++ { + expected := strings.ContainsAny(good, string([]byte{byte(i)})) + Expect(s.ascii[i]).To(Equal(expected), "checking %d", i) + } + }) + }) + Describe("quotingFormat", func() { + const badQuoteMode = QuoteMode(0xBB) + + T.DescribeTable("should return proper formatting string", + func(mode QuoteMode, msg string, expected string) { + s.QuoteMode = mode + Expect(s.quotingFormat(msg)).To(Equal(expected)) + }, + T.Entry("None/empty", QuoteModeNone, empty, "%s"), + T.Entry("None/nospecial", QuoteModeNone, nospecial, "%s"), + T.Entry("None/special", QuoteModeNone, special, "%s"), + T.Entry("None/mixed", QuoteModeNone, mixed, "%s"), + + T.Entry("SpecialAndEmpty/empty", QuoteModeSpecialAndEmpty, empty, "%q"), + T.Entry("SpecialAndEmpty/nospecial", QuoteModeSpecialAndEmpty, nospecial, "%s"), + T.Entry("SpecialAndEmpty/special", QuoteModeSpecialAndEmpty, special, "%q"), + T.Entry("SpecialAndEmpty/mixed", QuoteModeSpecialAndEmpty, mixed, "%q"), + + T.Entry("Special/empty", QuoteModeSpecial, empty, "%s"), + T.Entry("Special/nospecial", QuoteModeSpecial, nospecial, "%s"), + T.Entry("Special/special", QuoteModeSpecial, special, "%q"), + T.Entry("Special/mixed", QuoteModeSpecial, mixed, "%q"), + + T.Entry("All/empty", QuoteModeAll, empty, "%q"), + T.Entry("All/nospecial", QuoteModeAll, nospecial, "%q"), + T.Entry("All/special", QuoteModeAll, special, "%q"), + T.Entry("All/mixed", QuoteModeAll, mixed, "%q"), + + T.Entry("BAD/empty", badQuoteMode, empty, "%s"), + T.Entry("BAD/nospecial", badQuoteMode, nospecial, "%s"), + T.Entry("BAD/special", badQuoteMode, special, "%s"), + T.Entry("BAD/mixed", badQuoteMode, mixed, "%s"), + ) + }) + Describe("serialization helpers", func() { + var buf *bytes.Buffer + BeforeEach(func() { + buf = &bytes.Buffer{} + }) + Describe("appendTimestamp", func() { + It("should do nothing when time stamp mode is set to None", func() { + s.TimestampMode = TimestampModeNone + stamp := time.Now() + err := s.appendTimestamp(buf, &stamp) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.Len()).To(BeZero()) + }) + T.DescribeTable("should serialize proper diff when time stamp is set to Diff", + func(nanoseconds int, expected string) { + s.TimestampMode = TimestampModeDiff + stamp := s.baseTime.Add(time.Duration(nanoseconds) * time.Nanosecond) + err := s.appendTimestamp(buf, &stamp) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + }, + T.Entry("Second", 1000*1000*1000, "[1.000000] "), + T.Entry("Milisecond", 1000*1000, "[0.001000] "), + T.Entry("Microsecond", 1000, "[0.000001] "), + T.Entry("Nanosecond", 1, "[0.000000] "), + T.Entry("Custom", 1234567890, "[1.234568] "), + ) + T.DescribeTable("should serialize proper time when time stamp is set to Full", + func(days int, expected string) { + s.TimestampMode = TimestampModeFull + stamp := time.Unix(1234567890, 0).AddDate(0, 0, days) + err := s.appendTimestamp(buf, &stamp) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + }, + T.Entry("Day", 1, "[2009-02-15T00:31:30+01:00] "), + T.Entry("Week", 7, "[2009-02-21T00:31:30+01:00] "), + T.Entry("Month", 30, "[2009-03-16T00:31:30+01:00] "), + T.Entry("Year", 365, "[2010-02-14T00:31:30+01:00] "), + ) + T.DescribeTable("should return error if writing fails", + func(mode TimestampMode) { + w := NewFailingWriter(0, testError) + s.TimestampMode = mode + stamp := time.Now() + err := s.appendTimestamp(w, &stamp) + Expect(err).To(Equal(testError)) + }, + T.Entry("Diff", TimestampModeDiff), + T.Entry("Full", TimestampModeFull), + ) + }) + Describe("appendLevel", func() { + T.DescribeTable("should serialize proper level without colors", + func(level Level, expected string) { + s.UseColors = false + err := s.appendLevel(buf, level) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + }, + T.Entry("EmergLevel", EmergLevel, "[EME] "), + T.Entry("AlertLevel", AlertLevel, "[ALE] "), + T.Entry("CritLevel", CritLevel, "[CRI] "), + T.Entry("ErrLevel", ErrLevel, "[ERR] "), + T.Entry("WarningLevel", WarningLevel, "[WAR] "), + T.Entry("NoticeLevel", NoticeLevel, "[NOT] "), + T.Entry("InfoLevel", InfoLevel, "[INF] "), + T.Entry("DebugLevel", DebugLevel, "[DEB] "), + ) + T.DescribeTable("should serialize proper level with colors", + func(level Level, expected string) { + s.UseColors = true + err := s.appendLevel(buf, level) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + }, + T.Entry("EmergLevel", EmergLevel, "["+red+bold+invert+"EME"+off+"] "), + T.Entry("AlertLevel", AlertLevel, "["+cyan+bold+invert+"ALE"+off+"] "), + T.Entry("CritLevel", CritLevel, "["+magenta+bold+invert+"CRI"+off+"] "), + T.Entry("ErrLevel", ErrLevel, "["+red+bold+"ERR"+off+"] "), + T.Entry("WarningLevel", WarningLevel, "["+yellow+bold+"WAR"+off+"] "), + T.Entry("NoticeLevel", NoticeLevel, "["+blue+bold+"NOT"+off+"] "), + T.Entry("InfoLevel", InfoLevel, "["+green+bold+"INF"+off+"] "), + T.Entry("DebugLevel", DebugLevel, "["+bold+"DEB"+off+"] "), + ) + T.DescribeTable("should return error if writing fails", + func(useColors bool) { + w := NewFailingWriter(0, testError) + s.UseColors = useColors + err := s.appendLevel(w, ErrLevel) + Expect(err).To(Equal(testError)) + }, + T.Entry("WithColors", true), + T.Entry("WithoutColors", false), + ) + }) + Describe("appendMessage", func() { + T.DescribeTable("should serialize properly quoted message", + func(msg string, expected string) { + err := s.appendMessage(buf, msg) + Expect(err).NotTo(HaveOccurred()) + Expect(buf.String()).To(Equal(expected)) + }, + + T.Entry("empty", empty, "\"\" "), + T.Entry("nospecial", nospecial, nospecial+" "), + T.Entry("special", special, "\""+special+"\" "), + T.Entry("mixed", mixed, "\""+mixed+"\" "), + ) + It("should return error if writing fails", func() { + w := NewFailingWriter(0, testError) + err := s.appendMessage(w, special) + Expect(err).To(Equal(testError)) + }) + }) + Describe("appendProperties", func() { + It("should do nothing when properties are empty", func() { + p := Properties{} + err := s.appendProperties(buf, p) + + Expect(err).NotTo(HaveOccurred()) + Expect(buf.Len()).To(BeZero()) + }) + It("should serialize all key-value pairs without color", func() { + s.UseColors = false + p := Properties{ + "name": "Alice", + "hash": "#$%%@", + "male": true, + "age": 37, + "skills": Properties{ + "coding": 7, + }, + "issues": "", + } + err := s.appendProperties(buf, p) + + Expect(err).NotTo(HaveOccurred()) + expected := `{age:37;hash:"#$%%@";issues:"";male:true;name:Alice;skills:` + + `"map[coding:7]";}` + Expect(buf.String()).To(Equal(expected)) + }) + It("should serialize all key-value pairs with bolded key names", func() { + s.UseColors = true + p := Properties{ + "name": "Alice", + "hash": "#$%%@", + "male": true, + "age": 37, + "skills": Properties{ + "coding": 7, + }, + "issues": "", + } + err := s.appendProperties(buf, p) + + Expect(err).NotTo(HaveOccurred()) + expected := "{" + propkey + "age" + off + ":37;" + propkey + "hash" + off + + ":\"#$%%@\";" + propkey + "issues" + off + ":\"\";" + propkey + "male" + off + + ":true;" + propkey + "name" + off + ":Alice;" + propkey + "skills" + off + + ":\"map[coding:7]\";}" + Expect(buf.String()).To(Equal(expected)) + }) + T.DescribeTable("should return error if writing fails", + func(bytes int) { + w := NewFailingWriter(bytes, testError) + p := Properties{ + "name": "Alice", + } + s.UseColors = false + err := s.appendProperties(w, p) + Expect(err).To(Equal(testError)) + }, + // 0 1 + // 1234567890123 + // * * * * + // expected serialized properties string: {name:Alice;} + T.Entry("OpeningBracket", 1), + T.Entry("Key", 4), + T.Entry("Value", 10), + T.Entry("ClosingBracket", 13), + ) + }) + }) + Describe("serialize", func() { + T.DescribeTable("should return error if writing fails", + func(bytes int) { + w := NewFailingWriter(bytes, testError) + s.UseColors = false + s.baseTime = time.Unix(1234567890, 0) + entry := &Entry{ + Level: ErrLevel, + Message: "message", + Timestamp: s.baseTime.Add(time.Duration(11) * time.Minute), + Properties: Properties{ + "name": "Alice", + "hash": "#$%%@", + "male": true, + }, + } + err := s.serialize(entry, w) + Expect(err).To(Equal(testError)) + }, + // expected serialized properties string: + // [660.000000] [ERR] message {hash:"#$%%@";male:true;name:Alice;} + // 0 1 2 3 4 5 6 + // 123456789012345678901234567890123456789012345678901234567890123 + // * * * * + T.Entry("Timestamp", 7), + T.Entry("Level", 16), + T.Entry("Message", 24), + T.Entry("Properties", 43), + ) + It("should fail if Entry is invalid", func() { + w := NewFailingWriter(0, testError) + err := s.serialize(nil, w) + Expect(err).To(Equal(ErrInvalidEntry)) + }) + }) + Describe("Serialize", func() { + It("should serialize message with all elements", func() { + s.baseTime = time.Unix(1234567890, 0) + entry := &Entry{ + Level: ErrLevel, + Message: "message", + Timestamp: s.baseTime.Add(time.Duration(11) * time.Minute), + Properties: Properties{ + "name": "Alice", + "hash": "#$%%@", + "male": true, + }, + } + byt, err := s.Serialize(entry) + Expect(err).NotTo(HaveOccurred()) + expected := "[660.000000] [" + red + bold + "ERR" + off + "] message {" + propkey + + "hash" + off + ":\"#$%%@\";" + propkey + "male" + off + ":true;" + propkey + + "name" + off + ":Alice;}" + Expect(string(byt)).To(Equal(expected)) + }) + It("should fail if Entry is invalid", func() { + byt, err := s.Serialize(nil) + Expect(err).To(Equal(ErrInvalidEntry)) + Expect(byt).To(BeNil()) + }) + }) +}) -- cgit v1.2.3