patt/encoder.go
// Copyright (C) 2017, ccpaging <ccpaging@gmail.com>. All rights reserved.
// Copyright (c) 2016 Uber Technologies, Inc.
package patt
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/ccpaging/nxlog4go/driver"
)
// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
b[bp] = byte('0' + i)
*buf = append(*buf, b[bp:]...)
}
/** Encoder **/
// Encoder defines log recorder field encoder interface for external packages extending.
type Encoder interface {
// Open opens a new Encoder according type.
NewEncoder(typ string) Encoder
// Encode serializes log recorder field to the bytes buffer.
Encode(out *bytes.Buffer, r *driver.Recorder)
}
type nopEncoder struct{}
// NewNopEncoder creates no-op encoder.
func NewNopEncoder() Encoder { return &nopEncoder{} }
func (e *nopEncoder) NewEncoder(string) Encoder { return e }
func (e *nopEncoder) Encode(*bytes.Buffer, *driver.Recorder) {}
/** cached Time Encoder **/
const (
modeTime int = iota
modeDate
modeZone
)
type cacheTime struct {
mode int
encode func(buf *bytes.Buffer, t *time.Time)
y, mm, d int
dateCache []byte
dayFirst bool // true, dmy; false, mdy
sep byte // '-', '/', '.'
century bool
h, m, s int // the number of seconds elapsed since January 1, 1970 UTC
timeCache []byte
nos bool
us bool
loc *time.Location
zoneCache []byte
zfmt string
}
// NewDateEncoder creates a new date encoder.
func NewDateEncoder(typ string) Encoder {
e := &cacheTime{mode: modeDate}
return e.NewEncoder(typ)
}
// NewTimeEncoder creates a new time encoder.
func NewTimeEncoder(typ string) Encoder {
e := &cacheTime{mode: modeTime}
return e.NewEncoder(typ)
}
// NewZoneEncoder creates a new time zone encoder.
func NewZoneEncoder(typ string) Encoder {
e := &cacheTime{mode: modeZone}
return e.NewEncoder(typ)
}
func (e *cacheTime) NewEncoder(typ string) Encoder {
// Build a new encoder, clear cache and remember mode
e = &cacheTime{mode: e.mode}
switch e.mode {
case modeDate:
e.setDate(typ)
case modeZone:
e.setZone(typ)
case modeTime:
fallthrough
default:
e.setTime(typ)
}
return e
}
func (e *cacheTime) Encode(out *bytes.Buffer, r *driver.Recorder) {
e.encode(out, &r.Created)
}
/** Date Encoding **/
func (e *cacheTime) writeCacheDate(buf *bytes.Buffer, t *time.Time) bool {
y, m, d := t.Date()
if y == e.y && int(m) == e.mm && d == e.d && e.dateCache != nil {
buf.Write(e.dateCache)
return true
}
e.y, e.mm, e.d = y, int(m), d
return false
}
func (e *cacheTime) encoDate(buf *bytes.Buffer, t *time.Time) {
if e.writeCacheDate(buf, t) {
return
}
y, m, d := t.Date()
y %= 100
var b [16]byte
i := 0
if e.dayFirst {
b[i] = byte('0' + d/10)
b[i+1] = byte('0' + d%10)
b[i+2] = e.sep
i += 3
}
b[i] = byte('0' + m/10)
b[i+1] = byte('0' + m%10)
b[i+2] = e.sep
i += 3
if !e.dayFirst {
b[i] = byte('0' + d/10)
b[i+1] = byte('0' + d%10)
b[i+2] = e.sep
i += 3
}
b[i] = byte('0' + y/10)
b[i+1] = byte('0' + y%10)
i += 2
e.dateCache = b[:i]
buf.Write(e.dateCache)
}
func (e *cacheTime) encoCYMD(buf *bytes.Buffer, t *time.Time) {
if e.writeCacheDate(buf, t) {
return
}
y, m, d := t.Date()
c := y / 100
y %= 100
var b [16]byte
b[0] = byte('0' + c/10)
b[1] = byte('0' + c%10)
b[2] = byte('0' + y/10)
b[3] = byte('0' + y%10)
b[4] = e.sep
b[5] = byte('0' + m/10)
b[6] = byte('0' + m%10)
b[7] = e.sep
b[8] = byte('0' + d/10)
b[9] = byte('0' + d%10)
e.dateCache = b[:10]
buf.Write(e.dateCache)
}
func (e *cacheTime) setDate(typ string) {
switch typ {
case "dmy":
e.dayFirst = true
e.sep = '/'
e.encode = e.encoDate
return
case "mdy":
e.sep = '/'
e.encode = e.encoDate
return
case "cymdDash":
e.sep = '-'
case "cymdDot":
e.sep = '.'
case "cymdSlash":
fallthrough
default:
e.sep = '/'
}
e.encode = e.encoCYMD
}
/** Time Encoding **/
func (e *cacheTime) writeCacheTime(buf *bytes.Buffer, t *time.Time) bool {
h, m, s := t.Clock()
if s == e.s && m == e.m && h == e.h && e.timeCache != nil {
buf.Write(e.timeCache)
return true
}
e.h, e.m, e.s = h, m, s
return false
}
func (e *cacheTime) encoHMS(buf *bytes.Buffer, t *time.Time) {
if e.writeCacheTime(buf, t) {
return
}
hh, mm, ss := t.Clock()
var b [16]byte
b[0] = byte('0' + hh/10)
b[1] = byte('0' + hh%10)
b[2] = ':'
b[3] = byte('0' + mm/10)
b[4] = byte('0' + mm%10)
if e.nos {
e.timeCache = b[:5]
buf.Write(e.timeCache)
return
}
b[5] = ':'
b[6] = byte('0' + ss/10)
b[7] = byte('0' + ss%10)
e.timeCache = b[:8]
buf.Write(e.timeCache)
if e.us {
var us []byte
us = append(us, '.')
itoa(&us, t.Nanosecond()/1e3, 6)
buf.Write(us)
}
}
func (e *cacheTime) rfc3339Nano(buf *bytes.Buffer, t *time.Time) {
// 2006-01-02T15:04:05.000000Z07:00
e.encoCYMD(buf, t)
buf.WriteByte('T')
e.encoHMS(buf, t)
var b []byte
itoa(&b, t.Nanosecond(), 9)
// trim '0'
n := len(b)
for n > 0 && b[n-1] == '0' {
n--
}
if n > 0 {
buf.WriteByte('.')
buf.Write(b[:n])
}
e.encoZone(buf, t)
}
func (e *cacheTime) iso8601(buf *bytes.Buffer, t *time.Time) {
// 2006-01-02T15:04:05.000Z0700
e.encoCYMD(buf, t)
buf.WriteByte('T')
e.encoHMS(buf, t)
buf.WriteByte('.')
var b []byte
itoa(&b, t.Nanosecond()/1e6, 3)
buf.Write(b)
e.encoZone(buf, t)
}
func (e *cacheTime) setTime(s string) {
switch s {
case "iso8601":
e.century = true
e.sep = '-'
e.zfmt = "Z0700"
e.encode = e.iso8601
return
case "rfc3339nano":
e.century = true
e.sep = '-'
e.zfmt = "Z07:00"
e.encode = e.rfc3339Nano
return
case "hhmm":
e.nos = true
case "hms.us":
e.us = true
case "hms":
fallthrough
default:
}
e.encode = e.encoHMS
}
/* Zone Encoder */
func (e *cacheTime) encoZone(buf *bytes.Buffer, t *time.Time) {
loc := t.Location()
if e.loc != loc || e.zoneCache == nil {
e.zoneCache = []byte(t.Format(e.zfmt))
e.loc = loc
}
buf.Write(e.zoneCache)
}
func (e *cacheTime) setZone(s string) {
switch s {
case "iso8601":
e.zfmt = "Z0700"
case "rfc3339":
e.zfmt = "Z07:00"
case "mst":
fallthrough
default:
e.zfmt = "MST"
}
e.encode = e.encoZone
}
/* Caller Encoder */
type callerEncoder struct {
mode int
}
const (
fullPath int = iota
shortPath
noPath
)
// NewCallerEncoder creates a new caller path encoder.
func NewCallerEncoder(typ string) Encoder {
e := &callerEncoder{}
return e.NewEncoder(typ)
}
func (*callerEncoder) NewEncoder(typ string) Encoder {
e := &callerEncoder{}
switch typ {
case "nopath":
e.mode = noPath
case "fullpath":
e.mode = fullPath
case "shortpath":
fallthrough
default:
e.mode = shortPath
}
return e
}
func (e *callerEncoder) Encode(buf *bytes.Buffer, r *driver.Recorder) {
s := r.Source
if len(s) <= 0 {
return
}
if e.mode == fullPath {
buf.WriteString(s)
return
}
// nb. To make sure we trim the path correctly on Windows too, we
// counter-intuitively need to use '/' and *not* os.PathSeparator here,
// because the path given originates from Go stdlib, specifically
// runtime.Caller() which (as of Mar/17) returns forward slashes even on
// Windows.
//
// See https://github.com/golang/go/issues/3335
// and https://github.com/golang/go/issues/18151
//
// for discussion on the issue on Go side.
//
// Find the last separator.
//
idx := strings.LastIndexByte(s, '/')
if idx == -1 {
buf.WriteString(s)
return
}
if e.mode != noPath {
// Find the penultimate separator.
idx = strings.LastIndexByte(s[:idx], '/')
if idx == -1 {
buf.WriteString(s)
return
}
}
buf.WriteString(s[idx+1:])
}
/* Fields Encoder */
type fieldsEncoder struct {
sep string
deli string
quote bool
encode func(out *bytes.Buffer, fields map[string]interface{}, index []string)
}
// NewFieldsEncoder creates a new fields encoder.
func NewFieldsEncoder(typ string) Encoder {
e := &fieldsEncoder{}
return e.NewEncoder(typ)
}
func (*fieldsEncoder) NewEncoder(typ string) Encoder {
e := &fieldsEncoder{}
switch typ {
case "json":
e.encode = e.encoJSON
return e
case "csv":
e.sep = "|"
e.deli = "="
case "quote":
e.sep = " "
e.deli = "="
e.quote = true
case "std":
fallthrough
default:
e.sep = " "
e.deli = "="
}
e.encode = e.encoStd
return e
}
func (e *fieldsEncoder) Encode(out *bytes.Buffer, r *driver.Recorder) {
fields, index := r.Fields()
e.encode(out, fields, index)
}
func (e *fieldsEncoder) encoKeyValue(out *bytes.Buffer, k string, v interface{}) {
out.WriteString(k + e.deli)
var s string
if e.quote {
s = fmt.Sprintf("%q", v)
} else {
if _, ok := v.(string); ok {
s = v.(string)
} else {
s = fmt.Sprint(v)
}
}
out.WriteString(s)
}
func (e *fieldsEncoder) encoStd(out *bytes.Buffer, fields map[string]interface{}, index []string) {
if len(fields) <= 0 {
return
}
if len(index) > 1 {
for _, k := range index {
out.WriteString(e.sep)
e.encoKeyValue(out, k, fields[k])
}
return
}
for k, v := range fields {
out.WriteString(e.sep)
e.encoKeyValue(out, k, v)
}
}
func (e *fieldsEncoder) encoJSON(out *bytes.Buffer, fields map[string]interface{}, index []string) {
if len(fields) <= 0 {
return
}
out.WriteString(",\"Fields\":")
encoder := json.NewEncoder(out)
encoder.Encode(fields)
}
/* Values Encoder */
type valuesEncoder struct {
sep string
quote bool
encode func(out *bytes.Buffer, values []interface{})
}
// NewValuesEncoder creates a new data fields encoder.
func NewValuesEncoder(typ string) Encoder {
e := &valuesEncoder{}
return e.NewEncoder(typ)
}
func (e *valuesEncoder) Encode(out *bytes.Buffer, r *driver.Recorder) {
e.encode(out, r.Values)
}
func (*valuesEncoder) NewEncoder(typ string) Encoder {
e := &valuesEncoder{}
switch typ {
case "json":
e.encode = e.encoJSON
return e
case "csv":
e.sep = "|"
case "quote":
e.sep = " "
e.quote = true
case "std":
fallthrough
default:
e.sep = " "
}
e.encode = e.encoStd
return e
}
func (e *valuesEncoder) encoStd(out *bytes.Buffer, values []interface{}) {
if len(values) <= 0 {
return
}
var s string
for _, v := range values {
out.WriteString(e.sep)
if e.quote {
s = fmt.Sprintf("%q", v)
} else {
if _, ok := v.(string); ok {
s = v.(string)
} else {
s = fmt.Sprint(v)
}
}
out.WriteString(s)
}
}
func (e *valuesEncoder) encoJSON(out *bytes.Buffer, values []interface{}) {
if len(values) <= 0 {
return
}
out.WriteString(",\"Values\":")
encoder := json.NewEncoder(out)
encoder.Encode(values)
}