- add Formatter.JSONEscapeHTML and SetJSONEscapeHTML to let JSON writers preserve HTML-sensitive characters when needed while keeping escaping enabled by default. - switch JSON output to json.Encoder, preserve existing Print/Println newline semantics, and cover the new escaping behavior in tests.
This commit is contained in:
@@ -1,3 +1,26 @@
|
|||||||
|
# v2.3.0
|
||||||
|
|
||||||
|
This release adds JSON HTML-escaping control while preserving the existing
|
||||||
|
JSON writer defaults and newline semantics.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `Formatter.SetJSONEscapeHTML(bool)` and `Formatter.JSONEscapeHTML` to control whether JSON writers escape HTML-sensitive characters such as `<`, `>`, and `&`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- JSON writers now use `json.Encoder` internally so formatter-level HTML escaping can be configured.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Preserved `Print` and `Println` newline behavior for JSON output while switching to `json.Encoder`.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
|
||||||
|
- No breaking API changes.
|
||||||
|
- HTML escaping remains enabled by default, matching the previous `encoding/json` behavior.
|
||||||
|
- `Print` still emits JSON without an added newline, and `Println` emits exactly one trailing newline.
|
||||||
|
|
||||||
# v2.2.0
|
# v2.2.0
|
||||||
|
|
||||||
This release expands the public LogLevel API, adds optional threshold-based
|
This release expands the public LogLevel API, adds optional threshold-based
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func main() {
|
|||||||
jsonFormatter := sneklog.NewFormatter().
|
jsonFormatter := sneklog.NewFormatter().
|
||||||
SetFormat("[%L] [%N] %m").
|
SetFormat("[%L] [%N] %m").
|
||||||
SetColorOutput(false).
|
SetColorOutput(false).
|
||||||
|
SetJSONEscapeHTML(false).
|
||||||
SetTimeStampFormat(sneklog.Kitchen)
|
SetTimeStampFormat(sneklog.Kitchen)
|
||||||
|
|
||||||
textStdout.SetFormatter(formatter)
|
textStdout.SetFormatter(formatter)
|
||||||
@@ -62,6 +63,7 @@ func main() {
|
|||||||
externalJSON,
|
externalJSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.Infoln("Тест <>&")
|
||||||
logger.Infoln("service started")
|
logger.Infoln("service started")
|
||||||
logger.Warnln("cache miss")
|
logger.Warnln("cache miss")
|
||||||
logger.Errorln("request failed")
|
logger.Errorln("request failed")
|
||||||
|
|||||||
+18
-3
@@ -32,9 +32,10 @@ type Formatter struct {
|
|||||||
Format string
|
Format string
|
||||||
MessageSeparator string
|
MessageSeparator string
|
||||||
|
|
||||||
// JSONTraceBackPath
|
// JSONTraceBackPath enables full file paths in formatted traceback fields.
|
||||||
// If true, in traceback will be path to file, otherwise only filename.
|
JSONTraceBackPath bool
|
||||||
JSONTraceBackPath bool
|
// JSONEscapeHTML controls whether JSON output escapes HTML-sensitive characters.
|
||||||
|
JSONEscapeHTML bool
|
||||||
TimeStampFormat string
|
TimeStampFormat string
|
||||||
TraceBackFormat string
|
TraceBackFormat string
|
||||||
TraceBackSeparator string
|
TraceBackSeparator string
|
||||||
@@ -57,6 +58,7 @@ var DefaultTextFormatter = &Formatter{
|
|||||||
// DefaultJsonFormatter is the default formatter used by JSON writers.
|
// DefaultJsonFormatter is the default formatter used by JSON writers.
|
||||||
var DefaultJsonFormatter = &Formatter{
|
var DefaultJsonFormatter = &Formatter{
|
||||||
Format: "%m",
|
Format: "%m",
|
||||||
|
JSONEscapeHTML: true,
|
||||||
MessageSeparator: " ",
|
MessageSeparator: " ",
|
||||||
TimeStampFormat: RFC3339,
|
TimeStampFormat: RFC3339,
|
||||||
ColorOutput: false,
|
ColorOutput: false,
|
||||||
@@ -67,6 +69,7 @@ func NewFormatter() *Formatter {
|
|||||||
return &Formatter{
|
return &Formatter{
|
||||||
Format: DefaultTextFormatter.Format,
|
Format: DefaultTextFormatter.Format,
|
||||||
MessageSeparator: DefaultTextFormatter.MessageSeparator,
|
MessageSeparator: DefaultTextFormatter.MessageSeparator,
|
||||||
|
JSONEscapeHTML: true,
|
||||||
TimeStampFormat: DefaultTextFormatter.TimeStampFormat,
|
TimeStampFormat: DefaultTextFormatter.TimeStampFormat,
|
||||||
TraceBackFormat: DefaultTextFormatter.TraceBackFormat,
|
TraceBackFormat: DefaultTextFormatter.TraceBackFormat,
|
||||||
TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator,
|
TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator,
|
||||||
@@ -87,6 +90,18 @@ func (f *Formatter) SetMessageSeparator(separator string) *Formatter {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetJSONTraceBackPath enables or disables full file paths in formatted traceback fields.
|
||||||
|
func (f *Formatter) SetJSONTraceBackPath(set bool) *Formatter {
|
||||||
|
f.JSONTraceBackPath = set
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetJSONEscapeHTML enables or disables HTML escaping in JSON output.
|
||||||
|
func (f *Formatter) SetJSONEscapeHTML(escape bool) *Formatter {
|
||||||
|
f.JSONEscapeHTML = escape
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
// SetTimeStampFormat sets the strftime-like format used for timestamps.
|
// SetTimeStampFormat sets the strftime-like format used for timestamps.
|
||||||
func (f *Formatter) SetTimeStampFormat(format string) *Formatter {
|
func (f *Formatter) SetTimeStampFormat(format string) *Formatter {
|
||||||
f.TimeStampFormat = format
|
f.TimeStampFormat = format
|
||||||
|
|||||||
@@ -342,6 +342,42 @@ func TestJsonWriterPrintAllowsEmptyMessages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJsonWriterPrintDoesNotAppendNewline(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := CreateJsonWriter(&buf, false)
|
||||||
|
|
||||||
|
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
|
||||||
|
t.Fatalf("Print() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasSuffix(buf.Bytes(), []byte("\n")) {
|
||||||
|
t.Fatalf("Print() should not append newline, got %q", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonWriterSetJSONEscapeHTML(t *testing.T) {
|
||||||
|
var escaped bytes.Buffer
|
||||||
|
escapedWriter := CreateJsonWriter(&escaped, false)
|
||||||
|
|
||||||
|
if err := escapedWriter.Print(INFO, "TEST", nil, "<>&"); err != nil {
|
||||||
|
t.Fatalf("Print() with default formatter error = %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(escaped.String(), "<>&") {
|
||||||
|
t.Fatalf("default JSON output should escape HTML-sensitive characters, got %q", escaped.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw bytes.Buffer
|
||||||
|
rawWriter := CreateJsonWriter(&raw, false).
|
||||||
|
SetFormatter(NewFormatter().SetFormat("%m").SetJSONEscapeHTML(false))
|
||||||
|
|
||||||
|
if err := rawWriter.Print(INFO, "TEST", nil, "<>&"); err != nil {
|
||||||
|
t.Fatalf("Print() with SetJSONEscapeHTML(false) error = %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(raw.String(), "<>&") {
|
||||||
|
t.Fatalf("SetJSONEscapeHTML(false) should preserve HTML-sensitive characters, got %q", raw.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
|
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
writer := CreateJsonWriter(&buf, false)
|
writer := CreateJsonWriter(&buf, false)
|
||||||
@@ -354,6 +390,9 @@ func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
|
|||||||
if !bytes.HasSuffix(data, []byte("\n")) {
|
if !bytes.HasSuffix(data, []byte("\n")) {
|
||||||
t.Fatalf("JSON output should end with newline, got %q", data)
|
t.Fatalf("JSON output should end with newline, got %q", data)
|
||||||
}
|
}
|
||||||
|
if bytes.HasSuffix(bytes.TrimSuffix(data, []byte("\n")), []byte("\n")) {
|
||||||
|
t.Fatalf("JSON output should end with exactly one newline, got %q", data)
|
||||||
|
}
|
||||||
|
|
||||||
var message LoggerJsonMessage
|
var message LoggerJsonMessage
|
||||||
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
|
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
|
||||||
|
|||||||
+16
-9
@@ -1,6 +1,7 @@
|
|||||||
package sneklog
|
package sneklog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -145,21 +146,27 @@ func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*Met
|
|||||||
Message: s,
|
Message: s,
|
||||||
Traceback: traceback,
|
Traceback: traceback,
|
||||||
}
|
}
|
||||||
var data []byte
|
|
||||||
var err error
|
buf := bytes.NewBuffer(nil)
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
enc.SetEscapeHTML(f.JSONEscapeHTML)
|
||||||
if w.pretty {
|
if w.pretty {
|
||||||
data, err = json.MarshalIndent(m, "", " ")
|
enc.SetIndent("", " ")
|
||||||
} else {
|
|
||||||
data, err = json.Marshal(m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := enc.Encode(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if newline {
|
data := buf.Bytes()
|
||||||
data = append(data, '\n')
|
if !newline {
|
||||||
|
data = bytes.TrimSuffix(data, []byte("\n"))
|
||||||
}
|
}
|
||||||
_, err = w.Write(data)
|
_, err = w.writer.Write(data)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the owned writer, if any.
|
// Close closes the owned writer, if any.
|
||||||
|
|||||||
Reference in New Issue
Block a user