github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekalog/encoder_json.go (about)

     1  // Copyright © 2020-2021. All rights reserved.
     2  // Author: Ilya Stroy.
     3  // Contacts: iyuryevich@pm.me, https://github.com/qioalice
     4  // License: https://opensource.org/licenses/MIT
     5  
     6  package ekalog
     7  
     8  import (
     9  	"time"
    10  
    11  	"github.com/qioalice/ekago/v3/internal/ekaletter"
    12  
    13  	"github.com/json-iterator/go"
    14  )
    15  
    16  //noinspection GoSnakeCaseUsage
    17  type (
    18  	// CI_JSONEncoder is a type that built to be used as a part of CommonIntegrator
    19  	// as an log Entry encoder to the some output as JSON.
    20  	// Custom indentation supported.
    21  	//
    22  	// If you want to use CI_JSONEncoder, you need to instantiate object,
    23  	// set indentation (if you need, default is 0: no indentation) and that is.
    24  	// The last thing you need to do is to register CI_ConsoleEncoder with
    25  	// CommonIntegrator using CommonIntegrator.WithEncoder().
    26  	//
    27  	// You MUST NOT to call EncodeEntry() method manually.
    28  	// It is used by associated CommonIntegrator and it WILL lead to UB
    29  	// if you will try to use it manually. May even panic.
    30  	CI_JSONEncoder struct {
    31  
    32  		// You know what "JSON indent" is (pretty JSON, etc), right?
    33  		// How much spaces will be added to the beginning of line for each
    34  		// nested JSON entity for each nesting level.
    35  		//
    36  		// So, for indent == 4, you will get:
    37  		//
    38  		// 		{
    39  		// 		    "key1": "value",
    40  		// 		    "nested": {
    41  		// 		        "nested_key": "value"
    42  		// 		    }
    43  		// 		}
    44  		//
    45  		// So, keys for 1st nesting level JSON entities has 4 spaces before
    46  		// ("key1", "nested") and for 2nd nesting level - 8 spaces before
    47  		// ("nested_key").
    48  		//
    49  		// You may set this value using SetIndent() method.
    50  		indent int
    51  
    52  		oneDepthLevel bool
    53  
    54  		// api is jsoniter's API object.
    55  		// Created at the first doBuild() call for object.
    56  		api jsoniter.API
    57  
    58  		preEncodedFieldsStreamIndentX1 *jsoniter.Stream
    59  		preEncodedFieldsStreamIndentX2 *jsoniter.Stream
    60  
    61  		fieldNames map[CI_JSONEncoder_Field]string
    62  
    63  		timeFormatter func(t time.Time) string
    64  	}
    65  
    66  	// CI_JSONEncoder_Field is a special type that represents a type of CI_JSONEncoder
    67  	// field names.
    68  	// This type exist to declare corresponding constants and be able to change
    69  	// default field's names to their user-defined alternatives.
    70  	CI_JSONEncoder_Field uint8
    71  )
    72  
    73  //noinspection GoSnakeCaseUsage
    74  const (
    75  	CI_JSON_ENCODER_FIELD_LEVEL CI_JSONEncoder_Field = 1 + iota
    76  	CI_JSON_ENCODER_FIELD_LEVEL_VALUE
    77  	CI_JSON_ENCODER_FIELD_TIME
    78  	CI_JSON_ENCODER_FIELD_MESSAGE
    79  	CI_JSON_ENCODER_FIELD_ERROR_ID
    80  	CI_JSON_ENCODER_FIELD_ERROR_CLASS_ID
    81  	CI_JSON_ENCODER_FIELD_ERROR_CLASS_NAME
    82  	CI_JSON_ENCODER_FIELD_STACKTRACE
    83  	CI_JSON_ENCODER_FIELD_1DL_STACKTRACE_MESSAGES
    84  	CI_JSON_ENCODER_FIELD_FIELDS
    85  	CI_JSON_ENCODER_FIELD_1DL_LOG_FIELDS_PREFIX
    86  	CI_JSON_ENCODER_FIELD_1DL_STACKTRACE_FIELDS_PREFIX
    87  )
    88  
    89  //noinspection GoSnakeCaseUsage
    90  const (
    91  	CI_JSON_ENCODER_FIELD_DEFAULT_LEVEL                        = "level"
    92  	CI_JSON_ENCODER_FIELD_DEFAULT_LEVEL_VALUE                  = "level_value"
    93  	CI_JSON_ENCODER_FIELD_DEFAULT_TIME                         = "time"
    94  	CI_JSON_ENCODER_FIELD_DEFAULT_MESSAGE                      = "message"
    95  	CI_JSON_ENCODER_FIELD_DEFAULT_ERROR_ID                     = "error_id"
    96  	CI_JSON_ENCODER_FIELD_DEFAULT_ERROR_CLASS_ID               = "error_class_id"
    97  	CI_JSON_ENCODER_FIELD_DEFAULT_ERROR_CLASS_NAME             = "error_class_name"
    98  	CI_JSON_ENCODER_FIELD_DEFAULT_STACKTRACE                   = "stacktrace"
    99  	CI_JSON_ENCODER_FIELD_DEFAULT_1DL_STACKTRACE_MESSAGES      = "stacktrace_messages"
   100  	CI_JSON_ENCODER_FIELD_DEFAULT_FIELDS                       = "fields"
   101  	CI_JSON_ENCODER_FIELD_DEFAULT_1DL_LOG_FIELDS_PREFIX        = "field_"
   102  	CI_JSON_ENCODER_FIELD_DEFAULT_1DL_STACKTRACE_FIELDS_PREFIX = "field_stacktrace_{{num}}_"
   103  )
   104  
   105  var (
   106  	// Make sure we won't break API.
   107  	_ CI_Encoder = (*CI_JSONEncoder)(nil)
   108  )
   109  
   110  // SetOneDepthLevel sets a depth level of an output of CI_JSONEncoder.
   111  //
   112  // By default, the depth level is > 1, meaning an output may look like:
   113  //
   114  // 		{
   115  // 		    "message": "Error message",
   116  // 		    "fields": {
   117  // 		        "key1": "value1" // <-- here 2 depth level
   118  // 		    }
   119  // 		    "stacktrace": [{
   120  // 		        "func": "ekago.v3.ekalog_test.foo", // <-- here 2 depth level
   121  // 		        "file": "logger_test.go:22",
   122  // 		        "package": "github.com/qioalice",
   123  // 		        "fields": {
   124  // 		            "test": 42 // <-- here 3 depth level
   125  // 		        }
   126  // 		    }]
   127  // 		}
   128  //
   129  // But enabling 1 depth level you will get:
   130  //
   131  // 		{
   132  // 		    "message": "Error message",
   133  // 		    "field_key1": "value1",
   134  // 		    "stacktrace": [
   135  // 		        "github.com/qioalice/ekago.v3.ekalog_test.foo (logger_test.go:22)"
   136  // 		    ],
   137  // 		    "field_stacktrace_0_test": 42
   138  // 		}
   139  //
   140  // Calling this method many times will overwrite previous value.
   141  //
   142  // This method MUST NOT be called after CI_JSONEncoder is registered
   143  // with CommonIntegrator using CommonIntegrator.WithEncoder() method.
   144  func (je *CI_JSONEncoder) SetOneDepthLevel(enable bool) *CI_JSONEncoder {
   145  
   146  	je.oneDepthLevel = enable
   147  	return je
   148  }
   149  
   150  // SetIndent sets an indentation of JSON encoding format.
   151  // Any value <= 0 meaning "no indentation".
   152  //
   153  // Calling this method many times will overwrite previous value of format string.
   154  //
   155  // This method MUST NOT be called after CI_JSONEncoder is registered
   156  // with CommonIntegrator using CommonIntegrator.WithEncoder() method.
   157  func (je *CI_JSONEncoder) SetIndent(num int) *CI_JSONEncoder {
   158  
   159  	if num < 0 {
   160  		num = 0
   161  	}
   162  	je.indent = num
   163  	return je
   164  }
   165  
   166  // SetNameForField allows you to rename default name for some fields.
   167  //
   168  // Keep in mind, using this method you can overwrite SYSTEM field's names
   169  // (the names of those fields that are ekalog.Entry contains).
   170  // You CANNOT change the name of user-added fields to ekalog.Entry.
   171  //
   172  // Calling this method many times with the same `fieldType`
   173  // will overwrite previous value.
   174  //
   175  // This method MUST NOT be called after CI_JSONEncoder is registered
   176  // with CommonIntegrator using CommonIntegrator.WithEncoder() method.
   177  func (je *CI_JSONEncoder) SetNameForField(fieldType CI_JSONEncoder_Field, name string) *CI_JSONEncoder {
   178  
   179  	if je.fieldNames == nil {
   180  		je.fieldNames = make(map[CI_JSONEncoder_Field]string)
   181  	}
   182  	je.fieldNames[fieldType] = name
   183  	return je
   184  }
   185  
   186  // SetTimeFormatter allows you to set formatter that will be encode `time` field
   187  // of the ekalog.Entry. Presented `formatter` MUST BE not nil, ignored otherwise.
   188  //
   189  // Calling this method many times will overwrite previous value of formatter.
   190  //
   191  // This method MUST NOT be called after CI_JSONEncoder is registered
   192  // with CommonIntegrator using CommonIntegrator.WithEncoder() method.
   193  func (je *CI_JSONEncoder) SetTimeFormatter(formatter func(t time.Time) string) *CI_JSONEncoder {
   194  
   195  	if formatter != nil {
   196  		je.timeFormatter = formatter
   197  	}
   198  	return je
   199  }
   200  
   201  // PreEncodeField allows you to pre-encode some ekaletter.LetterField,
   202  // that is must be used with EACH Entry that will be encoded using this CI_JSONEncoder.
   203  //
   204  // It's useful when you want some unchanged runtime data for each log message,
   205  // like git hash commit, version, etc. Or if you want to create many loggers
   206  // attach some different fields to them and log different logs using them.
   207  //
   208  // Unnamed fields are not allowed.
   209  //
   210  // By default, encoded field will be added to the "fields" root section.
   211  // If you want to place field directly to the root section,
   212  // use ekaletter.KIND_FLAG_USER_DEFINED for ekaletter.LetterField's Kind property
   213  // (set it).
   214  //
   215  // WARNING!
   216  // PreEncodeField() MUST BE USED ONLY IF CI_JSONEncoder HAS BEEN REGISTERED
   217  // WITH SOME CommonIntegrator ALREADY. UB OTHERWISE, MAY PANIC!
   218  func (je *CI_JSONEncoder) PreEncodeField(f ekaletter.LetterField) {
   219  
   220  	// Avoid calls of PreEncodeField() when CI_ConsoleEncoder has not built yet.
   221  	if f.Key == "" || je.api == nil || f.IsInvalid() || f.RemoveVary() && f.IsZero() {
   222  		return
   223  	}
   224  
   225  	stream := je.preEncodedFieldsStreamIndentX2
   226  	if f.Kind&ekaletter.KIND_FLAG_USER_DEFINED != 0 {
   227  		stream = je.preEncodedFieldsStreamIndentX1
   228  	}
   229  
   230  	if wasAdded := je.encodeField(stream, f); wasAdded {
   231  		stream.WriteMore()
   232  	}
   233  }
   234  
   235  // EncodeEntry encodes passed Entry in JSON format using provided indentation.
   236  //
   237  // EncodeEntry is for internal purposes only and MUST NOT be called directly.
   238  // UB otherwise, may panic.
   239  func (je *CI_JSONEncoder) EncodeEntry(e *Entry) []byte {
   240  
   241  	s := je.api.BorrowStream(nil)
   242  	defer je.api.ReturnStream(s)
   243  
   244  	// Use last ekaerr.Error's message as Entry's one if it's empty.
   245  	if e.ErrLetter != nil {
   246  		if l := len(e.ErrLetter.Messages); l > 0 && e.LogLetter.Messages[0].Body == "" {
   247  			e.LogLetter.Messages[0].Body = e.ErrLetter.Messages[l-1].Body
   248  			e.ErrLetter.Messages[l-1].Body = ""
   249  		}
   250  	}
   251  
   252  	s.WriteObjectStart()
   253  
   254  	je.encodeBase(s, e)
   255  	s.WriteMore()
   256  
   257  	// Write pre-encoded fields in root section
   258  	if b := je.preEncodedFieldsStreamIndentX1.Buffer(); len(b) > 0 {
   259  		s.SetBuffer(bufw2(s.Buffer(), b))
   260  		//s.WriteMore() // unnecessary, WriteMore() already called for field stream
   261  	}
   262  
   263  	// Handle special case when ekaerr.Error's ekaletter.Letter has a fields
   264  	// but has no stacktrace. It means that lightweight error has been created.
   265  	lightweightErrorFields := []ekaletter.LetterField(nil)
   266  	if e.ErrLetter != nil && len(e.ErrLetter.StackTrace) == 0 && len(e.ErrLetter.Fields) > 0 {
   267  		lightweightErrorFields = e.ErrLetter.Fields
   268  	}
   269  
   270  	if wasAdded := je.encodeFields(s, e.LogLetter.Fields, lightweightErrorFields, true); wasAdded {
   271  		s.WriteMore()
   272  	}
   273  
   274  	if wasAdded := je.encodeStacktrace(s, e); wasAdded {
   275  		s.WriteMore()
   276  	}
   277  
   278  	// ------------ Add new sections here ------------ //
   279  
   280  	// We writing the JSON's comma at the each section, expecting that the next
   281  	// section will be written too. But it might be an empty.
   282  	// So, we need to remove the last comma. There is no more sections to be written.
   283  	b := s.Buffer()
   284  	s.SetBuffer(b[:len(b)-1])
   285  
   286  	s.WriteObjectEnd()
   287  
   288  	b = s.Buffer()
   289  	copied := make([]byte, len(b)+1)
   290  	copy(copied, b)
   291  
   292  	copied[len(copied)-1] = '\n'
   293  
   294  	// Restore ekaerr.Error's last message that was used as Entry's message.
   295  	if e.ErrLetter != nil {
   296  		if l := len(e.ErrLetter.Messages); l > 0 && e.ErrLetter.Messages[l-1].Body == "" {
   297  			e.ErrLetter.Messages[l-1].Body = e.LogLetter.Messages[0].Body
   298  			e.LogLetter.Messages[0].Body = ""
   299  		}
   300  	}
   301  
   302  	return copied
   303  }