github.com/waldiirawan/apm-agent-go/v2@v2.2.2/modelwriter.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package apm // import "github.com/waldiirawan/apm-agent-go/v2" 19 20 import ( 21 "time" 22 23 "github.com/waldiirawan/apm-agent-go/v2/internal/ringbuffer" 24 "github.com/waldiirawan/apm-agent-go/v2/model" 25 "go.elastic.co/fastjson" 26 ) 27 28 const ( 29 transactionBlockTag ringbuffer.BlockTag = iota + 1 30 spanBlockTag 31 errorBlockTag 32 metricsBlockTag 33 ) 34 35 // notSampled is used as the pointee for the model.Transaction.Sampled field 36 // of non-sampled transactions. 37 var notSampled = false 38 39 type modelWriter struct { 40 buffer *ringbuffer.Buffer 41 metricsBuffer *ringbuffer.Buffer 42 cfg *tracerConfig 43 stats *TracerStats 44 json fastjson.Writer 45 modelStacktrace []model.StacktraceFrame 46 } 47 48 // writeTransaction encodes tx as JSON to the buffer, and then resets tx. 49 func (w *modelWriter) writeTransaction(tx *Transaction, td *TransactionData) { 50 var modelTx model.Transaction 51 w.buildModelTransaction(&modelTx, tx, td) 52 w.json.RawString(`{"transaction":`) 53 modelTx.MarshalFastJSON(&w.json) 54 w.json.RawByte('}') 55 w.buffer.WriteBlock(w.json.Bytes(), transactionBlockTag) 56 w.json.Reset() 57 td.reset(tx.tracer) 58 } 59 60 // writeSpan encodes s as JSON to the buffer, and then resets s. 61 func (w *modelWriter) writeSpan(s *Span, sd *SpanData) { 62 var modelSpan model.Span 63 w.buildModelSpan(&modelSpan, s, sd) 64 w.json.RawString(`{"span":`) 65 modelSpan.MarshalFastJSON(&w.json) 66 w.json.RawByte('}') 67 w.buffer.WriteBlock(w.json.Bytes(), spanBlockTag) 68 w.json.Reset() 69 sd.reset(s.tracer) 70 } 71 72 // writeError encodes e as JSON to the buffer, and then resets e. 73 func (w *modelWriter) writeError(e *ErrorData) { 74 var modelError model.Error 75 w.buildModelError(&modelError, e) 76 w.json.RawString(`{"error":`) 77 modelError.MarshalFastJSON(&w.json) 78 w.json.RawByte('}') 79 w.buffer.WriteBlock(w.json.Bytes(), errorBlockTag) 80 w.json.Reset() 81 e.reset() 82 } 83 84 // writeMetrics encodes m as JSON to the w.metricsBuffer, and then resets m. 85 // 86 // Note that we do not write metrics to the main ring buffer (w.buffer), as 87 // periodic metrics would be evicted by transactions/spans in a busy system. 88 func (w *modelWriter) writeMetrics(m *Metrics) { 89 for _, m := range m.transactionGroupMetrics { 90 w.json.RawString(`{"metricset":`) 91 m.MarshalFastJSON(&w.json) 92 w.json.RawString("}") 93 w.metricsBuffer.WriteBlock(w.json.Bytes(), metricsBlockTag) 94 w.json.Reset() 95 } 96 for _, m := range m.metrics { 97 w.json.RawString(`{"metricset":`) 98 m.MarshalFastJSON(&w.json) 99 w.json.RawString("}") 100 w.metricsBuffer.WriteBlock(w.json.Bytes(), metricsBlockTag) 101 w.json.Reset() 102 } 103 m.reset() 104 } 105 106 func (w *modelWriter) buildModelTransaction(out *model.Transaction, tx *Transaction, td *TransactionData) { 107 out.ID = model.SpanID(tx.traceContext.Span) 108 out.TraceID = model.TraceID(tx.traceContext.Trace) 109 sampled := tx.traceContext.Options.Recorded() 110 if !sampled { 111 out.Sampled = ¬Sampled 112 } 113 if tx.traceContext.State.haveSampleRate { 114 out.SampleRate = &tx.traceContext.State.sampleRate 115 } 116 117 out.ParentID = model.SpanID(tx.parentID) 118 out.Name = truncateString(td.Name) 119 out.Type = truncateString(td.Type) 120 out.Result = truncateString(td.Result) 121 out.Outcome = normalizeOutcome(td.Outcome) 122 out.Timestamp = model.Time(td.timestamp.UTC()) 123 out.Duration = td.Duration.Seconds() * 1000 124 out.SpanCount.Started = td.spansCreated 125 out.SpanCount.Dropped = td.spansDropped 126 out.OTel = td.Context.otel 127 for _, sl := range td.links { 128 out.Links = append(out.Links, model.SpanLink{TraceID: model.TraceID(sl.Trace), SpanID: model.SpanID(sl.Span)}) 129 } 130 if dss := buildDroppedSpansStats(td.droppedSpansStats); len(dss) > 0 { 131 out.DroppedSpansStats = dss 132 } 133 134 if sampled { 135 out.Context = td.Context.build() 136 } 137 } 138 139 func (w *modelWriter) buildModelSpan(out *model.Span, span *Span, sd *SpanData) { 140 w.modelStacktrace = w.modelStacktrace[:0] 141 out.ID = model.SpanID(span.traceContext.Span) 142 out.TraceID = model.TraceID(span.traceContext.Trace) 143 out.TransactionID = model.SpanID(span.transactionID) 144 if span.traceContext.State.haveSampleRate { 145 out.SampleRate = &span.traceContext.State.sampleRate 146 } 147 148 out.ParentID = model.SpanID(span.parentID) 149 out.Name = truncateString(sd.Name) 150 out.Type = truncateString(sd.Type) 151 out.Subtype = truncateString(sd.Subtype) 152 out.Action = truncateString(sd.Action) 153 out.Timestamp = model.Time(sd.timestamp.UTC()) 154 out.Duration = sd.Duration.Seconds() * 1000 155 out.Outcome = normalizeOutcome(sd.Outcome) 156 out.Context = sd.Context.build() 157 out.OTel = sd.Context.otel 158 for _, sl := range sd.links { 159 out.Links = append(out.Links, model.SpanLink{TraceID: model.TraceID(sl.Trace), SpanID: model.SpanID(sl.Span)}) 160 } 161 if sd.composite.count > 1 { 162 out.Composite = sd.composite.build() 163 } 164 165 // Copy the span type to context.destination.service.type. 166 if out.Context != nil && out.Context.Destination != nil && out.Context.Destination.Service != nil { 167 out.Context.Destination.Service.Type = out.Type 168 } 169 170 w.modelStacktrace = appendModelStacktraceFrames(w.modelStacktrace, sd.stacktrace) 171 out.Stacktrace = w.modelStacktrace 172 } 173 174 func (w *modelWriter) buildModelError(out *model.Error, e *ErrorData) { 175 out.ID = model.TraceID(e.ID) 176 out.TraceID = model.TraceID(e.TraceID) 177 out.ParentID = model.SpanID(e.ParentID) 178 out.TransactionID = model.SpanID(e.TransactionID) 179 out.Timestamp = model.Time(e.Timestamp.UTC()) 180 out.Context = e.Context.build() 181 out.Culprit = e.Culprit 182 183 if !e.TransactionID.isZero() { 184 out.Transaction.Sampled = &e.transactionSampled 185 if e.transactionSampled { 186 out.Transaction.Type = e.transactionType 187 out.Transaction.Name = e.transactionName 188 } 189 } 190 191 // Create model stacktrace frames, and set the context. 192 w.modelStacktrace = w.modelStacktrace[:0] 193 var appendModelErrorStacktraceFrames func(exception *exceptionData) 194 appendModelErrorStacktraceFrames = func(exception *exceptionData) { 195 if len(exception.stacktrace) != 0 { 196 w.modelStacktrace = appendModelStacktraceFrames(w.modelStacktrace, exception.stacktrace) 197 } 198 for _, cause := range exception.cause { 199 appendModelErrorStacktraceFrames(&cause) 200 } 201 } 202 appendModelErrorStacktraceFrames(&e.exception) 203 if len(e.logStacktrace) != 0 { 204 w.modelStacktrace = appendModelStacktraceFrames(w.modelStacktrace, e.logStacktrace) 205 } 206 207 var modelStacktraceOffset int 208 if e.exception.message != "" { 209 var buildException func(exception *exceptionData) model.Exception 210 culprit := e.Culprit 211 buildException = func(exception *exceptionData) model.Exception { 212 out := model.Exception{ 213 Message: exception.message, 214 Code: model.ExceptionCode{ 215 String: exception.Code.String, 216 Number: exception.Code.Number, 217 }, 218 Type: exception.Type.Name, 219 Module: exception.Type.PackagePath, 220 Handled: e.Handled, 221 } 222 if n := len(exception.stacktrace); n != 0 { 223 out.Stacktrace = w.modelStacktrace[modelStacktraceOffset : modelStacktraceOffset+n] 224 modelStacktraceOffset += n 225 } 226 if len(exception.attrs) != 0 { 227 out.Attributes = exception.attrs 228 } 229 if n := len(exception.cause); n > 0 { 230 out.Cause = make([]model.Exception, n) 231 for i := range exception.cause { 232 out.Cause[i] = buildException(&exception.cause[i]) 233 } 234 } 235 if culprit == "" { 236 culprit = stacktraceCulprit(out.Stacktrace) 237 } 238 return out 239 } 240 out.Exception = buildException(&e.exception) 241 out.Culprit = culprit 242 } 243 if e.log.Message != "" { 244 out.Log = model.Log{ 245 Message: e.log.Message, 246 Level: e.log.Level, 247 LoggerName: e.log.LoggerName, 248 ParamMessage: e.log.MessageFormat, 249 } 250 if n := len(e.logStacktrace); n != 0 { 251 out.Log.Stacktrace = w.modelStacktrace[modelStacktraceOffset : modelStacktraceOffset+n] 252 modelStacktraceOffset += n 253 if out.Culprit == "" { 254 out.Culprit = stacktraceCulprit(out.Log.Stacktrace) 255 } 256 } 257 } 258 out.Culprit = truncateString(out.Culprit) 259 } 260 261 func stacktraceCulprit(frames []model.StacktraceFrame) string { 262 for _, frame := range frames { 263 if !frame.LibraryFrame { 264 return frame.Function 265 } 266 } 267 return "" 268 } 269 270 func normalizeOutcome(outcome string) string { 271 switch outcome { 272 case "success", "failure", "unknown": 273 return outcome 274 default: 275 return "unknown" 276 } 277 } 278 279 func buildDroppedSpansStats(dss droppedSpanTimingsMap) []model.DroppedSpansStats { 280 out := make([]model.DroppedSpansStats, 0, len(dss)) 281 for k, timing := range dss { 282 out = append(out, model.DroppedSpansStats{ 283 DestinationServiceResource: k.destination, 284 ServiceTargetType: k.serviceTargetType, 285 ServiceTargetName: k.serviceTargetName, 286 Outcome: normalizeOutcome(k.outcome), 287 Duration: model.AggregateDuration{ 288 Count: int(timing.count), 289 Sum: model.DurationSum{ 290 // The internal representation of spanTimingsMap is in time.Nanosecond 291 // unit which we need to convert to us. 292 Us: timing.duration / int64(time.Microsecond), 293 }, 294 }, 295 }) 296 } 297 return out 298 }