github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/integrations/prometheus/ingest/putmetrics.go (about)

     1  /*
     2  Copyright 2023.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package writer
    18  
    19  import (
    20  	"encoding/json"
    21  	"strconv"
    22  
    23  	"github.com/gogo/protobuf/proto"
    24  	"github.com/golang/snappy"
    25  	"github.com/prometheus/common/model"
    26  	"github.com/prometheus/prometheus/prompb"
    27  	. "github.com/siglens/siglens/pkg/segment/utils"
    28  	"github.com/siglens/siglens/pkg/segment/writer"
    29  	"github.com/siglens/siglens/pkg/usageStats"
    30  	"github.com/siglens/siglens/pkg/utils"
    31  	log "github.com/sirupsen/logrus"
    32  	"github.com/valyala/fasthttp"
    33  )
    34  
    35  type PrometheusPutResp struct {
    36  	Failed  uint64   `json:"failed"`
    37  	Success uint64   `json:"success"`
    38  	Errors  []string `json:"errors,omitempty"`
    39  }
    40  
    41  func decodeWriteRequest(compressed []byte) (*prompb.WriteRequest, error) {
    42  	reqBuf, err := snappy.Decode(nil, compressed)
    43  	if err != nil {
    44  		log.Errorf("decodeWriteRequest: Error decompressing request body, err: %v", err)
    45  		return nil, err
    46  	}
    47  	var req prompb.WriteRequest
    48  	if err := proto.Unmarshal(reqBuf, &req); err != nil {
    49  		log.Errorf("decodeWriteRequest: Error unmarshalling request body, err: %v", err)
    50  		return nil, err
    51  	}
    52  	return &req, nil
    53  }
    54  
    55  func PutMetrics(ctx *fasthttp.RequestCtx) {
    56  	var processedCount uint64
    57  	var failedCount uint64
    58  	var err error
    59  	version := string(ctx.Request.Header.Peek("X-Prometheus-Remote-Write-Version"))
    60  	if version != "0.1.0" {
    61  		log.Errorf("PutMetrics: Unsupported remote write protocol version %v", version)
    62  		writePrometheusResponse(ctx, processedCount, failedCount, "unsupported remote write protocol", fasthttp.StatusBadRequest)
    63  		return
    64  	}
    65  	cType := string(ctx.Request.Header.ContentType())
    66  	if cType != "application/x-protobuf" {
    67  		log.Errorf("PutMetrics: unknown content type [%s]! %v", cType, err)
    68  		writePrometheusResponse(ctx, processedCount, failedCount, "unknown content type", fasthttp.StatusBadRequest)
    69  		return
    70  	}
    71  	encoding := string(ctx.Request.Header.ContentEncoding())
    72  	if encoding != "snappy" {
    73  		log.Errorf("PutMetrics: unknown content encoding [%s]! %v", encoding, err)
    74  		writePrometheusResponse(ctx, processedCount, failedCount, "unknown content encoding", fasthttp.StatusBadRequest)
    75  		return
    76  	}
    77  
    78  	compressed := ctx.PostBody()
    79  	processedCount, failedCount, err = HandlePutMetrics(compressed)
    80  	if err != nil {
    81  		writePrometheusResponse(ctx, processedCount, failedCount, err.Error(), fasthttp.StatusBadRequest)
    82  		return
    83  	}
    84  	writePrometheusResponse(ctx, processedCount, failedCount, "", fasthttp.StatusOK)
    85  }
    86  
    87  func HandlePutMetrics(compressed []byte) (uint64, uint64, error) {
    88  	var successCount uint64 = 0
    89  	var failedCount uint64 = 0
    90  
    91  	req, err := decodeWriteRequest(compressed)
    92  	if err != nil {
    93  		return successCount, failedCount, nil
    94  	}
    95  
    96  	for _, ts := range req.Timeseries {
    97  		metric := make(model.Metric, len(ts.Labels))
    98  		for _, l := range ts.Labels {
    99  			metric[model.LabelName(l.Name)] = model.LabelValue(l.Value)
   100  		}
   101  
   102  		for _, s := range ts.Samples {
   103  			var sample model.Sample = model.Sample{
   104  				Metric:    metric,
   105  				Value:     model.SampleValue(s.Value),
   106  				Timestamp: model.Time(s.Timestamp),
   107  			}
   108  
   109  			data, err := sample.MarshalJSON()
   110  			if err != nil {
   111  				failedCount++
   112  				log.Errorf("HandlePutMetrics: failed to marshal data, err: %+v", err)
   113  				continue
   114  			}
   115  
   116  			var dataJson map[string]interface{}
   117  			err = json.Unmarshal(data, &dataJson)
   118  			if err != nil {
   119  				failedCount++
   120  				log.Errorf("HandlePutMetrics: failed to Unmarshal data, err: %+v", err)
   121  				continue
   122  			}
   123  
   124  			var metricName string
   125  			tags := "{"
   126  			for key, val := range dataJson {
   127  				if key == "metric" {
   128  					valMap, ok := val.(map[string]interface{})
   129  					if ok {
   130  						for k, v := range valMap {
   131  							if k == "__name__" {
   132  								valString, ok := v.(string)
   133  								if ok {
   134  									metricName = valString
   135  								}
   136  							}
   137  							valString, ok := v.(string)
   138  							if ok {
   139  								tags += `"` + k + `":"` + valString + `",`
   140  							}
   141  						}
   142  					}
   143  				}
   144  			}
   145  			tags += `"metric"` + `:"` + metricName + `",`
   146  			if tags[len(tags)-1] == ',' {
   147  				tags = tags[:len(tags)-1]
   148  			}
   149  			tags += "}"
   150  
   151  			modifiedData := `{"metric":"` + metricName + `","tags":` + tags + `,"timestamp":` + strconv.FormatInt(s.Timestamp, 10) + `,"value":` + strconv.FormatFloat(s.Value, 'f', -1, 64) + `}`
   152  
   153  			err = writer.AddTimeSeriesEntryToInMemBuf([]byte(modifiedData), SIGNAL_METRICS_OTSDB, uint64(0))
   154  			if err != nil {
   155  				log.Errorf("HandlePutMetrics: failed to add time series entry %+v", err)
   156  				failedCount++
   157  			} else {
   158  				successCount++
   159  			}
   160  		}
   161  	}
   162  	bytesReceived := uint64(len(compressed))
   163  	usageStats.UpdateMetricsStats(bytesReceived, successCount, 0)
   164  	return successCount, failedCount, nil
   165  }
   166  
   167  func writePrometheusResponse(ctx *fasthttp.RequestCtx, processedCount uint64, failedCount uint64, err string, code int) {
   168  
   169  	resp := PrometheusPutResp{Success: processedCount, Failed: failedCount}
   170  	if err != "" {
   171  		resp.Errors = []string{err}
   172  	}
   173  
   174  	ctx.SetStatusCode(code)
   175  	ctx.SetContentType(utils.ContentJson)
   176  	jval, mErr := json.Marshal(resp)
   177  	if mErr != nil {
   178  		log.Errorf("writePrometheusResponse: failed to marshal resp %+v", mErr)
   179  		return
   180  	}
   181  	_, mErr = ctx.Write(jval)
   182  
   183  	if mErr != nil {
   184  		log.Errorf("writePrometheusResponse: failed to write jval to http request %+v", mErr)
   185  		return
   186  	}
   187  }