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 }