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 = &notSampled
   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  }