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 }