github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/otlp/otlp.go (about)

     1  package otlp
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  
    11  	"github.com/siglens/siglens/pkg/es/writer"
    12  	"github.com/siglens/siglens/pkg/utils"
    13  	log "github.com/sirupsen/logrus"
    14  	"github.com/valyala/fasthttp"
    15  	coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
    16  	commonpb "go.opentelemetry.io/proto/otlp/common/v1"
    17  	tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
    18  	"google.golang.org/genproto/googleapis/rpc/status"
    19  	"google.golang.org/protobuf/proto"
    20  )
    21  
    22  func ProcessTraceIngest(ctx *fasthttp.RequestCtx) {
    23  	// All requests and responses should be protobufs.
    24  	ctx.Response.Header.Set("Content-Type", "application/x-protobuf")
    25  	if string(ctx.Request.Header.Peek("Content-Type")) != "application/x-protobuf" {
    26  		log.Infof("ProcessTraceIngest: got a non-protobuf request")
    27  		setFailureResponse(ctx, fasthttp.StatusBadRequest, "Expected a protobuf request")
    28  		return
    29  	}
    30  
    31  	// Get the data from the request.
    32  	data := ctx.PostBody()
    33  	if requiresGzipDecompression(ctx) {
    34  		reader, err := gzip.NewReader(bytes.NewReader(data))
    35  		if err != nil {
    36  			setFailureResponse(ctx, fasthttp.StatusBadRequest, "Unable to gzip decompress the data")
    37  			return
    38  		}
    39  
    40  		data, err = io.ReadAll(reader)
    41  		if err != nil {
    42  			setFailureResponse(ctx, fasthttp.StatusBadRequest, "Unable to gzip decompress the data")
    43  			return
    44  		}
    45  	}
    46  
    47  	// Unmarshal the data.
    48  	request, err := unmarshalTraceRequest(data)
    49  	if err != nil {
    50  		log.Errorf("ProcessTraceIngest: failed to unpack: %v", err)
    51  		setFailureResponse(ctx, fasthttp.StatusBadRequest, "Unable to unmarshal traces")
    52  		return
    53  	}
    54  
    55  	// Setup ingestion parameters.
    56  	now := utils.GetCurrentTimeInMs()
    57  	indexName := "traces"
    58  	shouldFlush := false
    59  	localIndexMap := make(map[string]string)
    60  	orgId := uint64(0)
    61  
    62  	// Go through the request data and ingest each of the spans.
    63  	numSpans := 0       // The total number of spans sent in this request.
    64  	numFailedSpans := 0 // The number of spans that we could not ingest.
    65  	for _, resourceSpans := range request.ResourceSpans {
    66  		// Find the service name.
    67  		var service string
    68  		if resourceSpans.Resource != nil {
    69  			for _, keyvalue := range resourceSpans.Resource.Attributes {
    70  				if keyvalue.Key == "service.name" {
    71  					service = keyvalue.Value.GetStringValue()
    72  				}
    73  			}
    74  		}
    75  
    76  		// Ingest each of these spans.
    77  		for _, scopeSpans := range resourceSpans.ScopeSpans {
    78  			numSpans += len(scopeSpans.Spans)
    79  			for _, span := range scopeSpans.Spans {
    80  				jsonData, err := spanToJson(span, service)
    81  				if err != nil {
    82  					log.Errorf("ProcessTraceIngest: failed to marshal span %s: %v", span, err)
    83  					numFailedSpans++
    84  					continue
    85  				}
    86  
    87  				lenJsonData := uint64(len(jsonData))
    88  				err = writer.ProcessIndexRequest(jsonData, now, indexName, lenJsonData, shouldFlush, localIndexMap, orgId)
    89  				if err != nil {
    90  					log.Errorf("ProcessTraceIngest: failed to process ingest request: %v", err)
    91  					numFailedSpans++
    92  					continue
    93  				}
    94  			}
    95  		}
    96  	}
    97  
    98  	log.Debugf("ProcessTraceIngest: %v spans in the request and failed to ingest %v of them", numSpans, numFailedSpans)
    99  
   100  	// Send the appropriate response.
   101  	handleTraceIngestionResponse(ctx, numSpans, numFailedSpans)
   102  }
   103  
   104  func requiresGzipDecompression(ctx *fasthttp.RequestCtx) bool {
   105  	encoding := string(ctx.Request.Header.Peek("Content-Encoding"))
   106  	if encoding == "gzip" {
   107  		return true
   108  	}
   109  
   110  	if encoding != "" && encoding != "none" {
   111  		log.Errorf("requiresGzipDecompression: invalid content encoding: %s", encoding)
   112  	}
   113  
   114  	return false
   115  }
   116  
   117  func unmarshalTraceRequest(data []byte) (*coltracepb.ExportTraceServiceRequest, error) {
   118  	var trace coltracepb.ExportTraceServiceRequest
   119  	err := proto.Unmarshal(data, &trace)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	return &trace, nil
   124  }
   125  
   126  func spanToJson(span *tracepb.Span, service string) ([]byte, error) {
   127  	result := make(map[string]interface{})
   128  	result["trace_id"] = hex.EncodeToString(span.TraceId)
   129  	result["span_id"] = hex.EncodeToString(span.SpanId)
   130  	result["parent_span_id"] = hex.EncodeToString(span.ParentSpanId)
   131  	result["service"] = service
   132  	result["trace_state"] = span.TraceState
   133  	result["name"] = span.Name
   134  	result["kind"] = span.Kind.String()
   135  	result["start_time"] = span.StartTimeUnixNano
   136  	result["end_time"] = span.EndTimeUnixNano
   137  	result["duration"] = span.EndTimeUnixNano - span.StartTimeUnixNano
   138  	result["dropped_attributes_count"] = uint64(span.DroppedAttributesCount)
   139  	result["dropped_events_count"] = uint64(span.DroppedEventsCount)
   140  	result["dropped_links_count"] = uint64(span.DroppedLinksCount)
   141  	if span.Status != nil {
   142  		result["status"] = span.Status.Code.String()
   143  	} else {
   144  		result["status"] = "Unknown"
   145  	}
   146  
   147  	// Make a column for each attribute key.
   148  	for _, keyvalue := range span.Attributes {
   149  		key, value, err := extractKeyValue(keyvalue)
   150  		if err != nil {
   151  			return nil, fmt.Errorf("spanToJson: failed to extract KeyValue: %v", err)
   152  		}
   153  
   154  		result[key] = value
   155  	}
   156  
   157  	eventsJson, err := json.Marshal(span.Events)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	result["events"] = string(eventsJson)
   162  
   163  	linksJson, err := linksToJson(span.Links)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	result["links"] = string(linksJson)
   168  
   169  	bytes, err := json.Marshal(result)
   170  	return bytes, err
   171  }
   172  
   173  func extractKeyValue(keyvalue *commonpb.KeyValue) (string, interface{}, error) {
   174  	value, err := extractAnyValue(keyvalue.Value)
   175  	if err != nil {
   176  		return "", nil, err
   177  	}
   178  
   179  	return keyvalue.Key, value, nil
   180  }
   181  
   182  func extractAnyValue(anyValue *commonpb.AnyValue) (interface{}, error) {
   183  	switch anyValue.Value.(type) {
   184  	case *commonpb.AnyValue_StringValue:
   185  		return anyValue.GetStringValue(), nil
   186  	case *commonpb.AnyValue_IntValue:
   187  		return anyValue.GetIntValue(), nil
   188  	case *commonpb.AnyValue_DoubleValue:
   189  		return anyValue.GetDoubleValue(), nil
   190  	case *commonpb.AnyValue_BoolValue:
   191  		return anyValue.GetBoolValue(), nil
   192  	case *commonpb.AnyValue_ArrayValue:
   193  		arrayValue := anyValue.GetArrayValue().Values
   194  		value := make([]interface{}, len(arrayValue))
   195  		for i := range arrayValue {
   196  			var err error
   197  			value[i], err = extractAnyValue(arrayValue[i])
   198  			if err != nil {
   199  				return nil, err
   200  			}
   201  		}
   202  
   203  		return value, nil
   204  	default:
   205  		return nil, fmt.Errorf("extractAnyValue: unsupported value type: %T", anyValue)
   206  	}
   207  }
   208  
   209  func linksToJson(spanLinks []*tracepb.Span_Link) ([]byte, error) {
   210  	// Links have SpanId and TraceId fields that we want to display has hex, so
   211  	// we need custom JSON marshalling.
   212  	type Link struct {
   213  		TraceId    string                 `json:"trace_id,omitempty"`
   214  		SpanId     string                 `json:"span_id,omitempty"`
   215  		TraceState string                 `json:"trace_state,omitempty"`
   216  		Attributes map[string]interface{} `json:"attributes,omitempty"`
   217  	}
   218  	links := make([]Link, len(spanLinks))
   219  
   220  	for i, link := range spanLinks {
   221  		attributes := make(map[string]interface{})
   222  		for _, keyvalue := range link.Attributes {
   223  			key, value, err := extractKeyValue(keyvalue)
   224  			if err != nil {
   225  				log.Errorf("spanToJson: failed to extract link attribute: %v", err)
   226  				return nil, err
   227  			}
   228  
   229  			attributes[key] = value
   230  		}
   231  
   232  		links[i] = Link{
   233  			TraceId:    string(link.TraceId),
   234  			SpanId:     string(link.SpanId),
   235  			TraceState: link.TraceState,
   236  			Attributes: attributes,
   237  		}
   238  	}
   239  
   240  	return json.Marshal(links)
   241  }
   242  
   243  func setFailureResponse(ctx *fasthttp.RequestCtx, statusCode int, message string) {
   244  	ctx.SetStatusCode(statusCode)
   245  
   246  	failureStatus := status.Status{
   247  		Code:    int32(statusCode),
   248  		Message: message,
   249  	}
   250  
   251  	bytes, err := proto.Marshal(&failureStatus)
   252  	if err != nil {
   253  		log.Errorf("sendFailureResponse: failed to marshal failure status: %v", err)
   254  	}
   255  	_, err = ctx.Write(bytes)
   256  	if err != nil {
   257  		log.Errorf("sendFailureResponse: failed to write failure status: %v", err)
   258  	}
   259  }
   260  
   261  func handleTraceIngestionResponse(ctx *fasthttp.RequestCtx, numSpans int, numFailedSpans int) {
   262  	if numFailedSpans == 0 {
   263  		// This request was successful.
   264  		response, err := proto.Marshal(&coltracepb.ExportTraceServiceResponse{})
   265  		if err != nil {
   266  			log.Errorf("ProcessTraceIngest: failed to marshal successful response: %v", err)
   267  			ctx.SetStatusCode(fasthttp.StatusInternalServerError)
   268  			return
   269  		}
   270  		_, err = ctx.Write(response)
   271  		if err != nil {
   272  			log.Errorf("ProcessTraceIngest: failed to write successful response: %v", err)
   273  			ctx.SetStatusCode(fasthttp.StatusInternalServerError)
   274  			return
   275  		}
   276  
   277  		ctx.SetStatusCode(fasthttp.StatusOK)
   278  		return
   279  	} else if numFailedSpans < numSpans {
   280  		// This request was partially successful.
   281  		traceResponse := coltracepb.ExportTraceServiceResponse{
   282  			PartialSuccess: &coltracepb.ExportTracePartialSuccess{
   283  				RejectedSpans: int64(numFailedSpans),
   284  			},
   285  		}
   286  
   287  		response, err := proto.Marshal(&traceResponse)
   288  		if err != nil {
   289  			log.Errorf("ProcessTraceIngest: failed to marshal partially successful response: %v", err)
   290  			ctx.SetStatusCode(fasthttp.StatusInternalServerError)
   291  			return
   292  		}
   293  		_, err = ctx.Write(response)
   294  		if err != nil {
   295  			log.Errorf("ProcessTraceIngest: failed to write partially successful response: %v", err)
   296  			ctx.SetStatusCode(fasthttp.StatusInternalServerError)
   297  			return
   298  		}
   299  
   300  		ctx.SetStatusCode(fasthttp.StatusOK)
   301  		return
   302  	} else {
   303  		// Every span failed to be ingested.
   304  		if numFailedSpans > numSpans {
   305  			log.Errorf("ProcessTraceIngest: error in counting number of total and failed spans")
   306  		}
   307  
   308  		setFailureResponse(ctx, fasthttp.StatusInternalServerError, "Every span failed ingestion")
   309  		return
   310  	}
   311  }