github.com/go-graphite/carbonapi@v0.17.0/cmd/mockbackend/render.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"log"
     8  	"math"
     9  	"math/rand"
    10  	"net/http"
    11  	"time"
    12  
    13  	"github.com/go-graphite/protocol/carbonapi_v2_pb"
    14  	"github.com/go-graphite/protocol/carbonapi_v3_pb"
    15  	ogórek "github.com/lomik/og-rek"
    16  	"go.uber.org/zap"
    17  
    18  	"github.com/go-graphite/carbonapi/zipper/httpHeaders"
    19  )
    20  
    21  func (cfg *listener) renderHandler(wr http.ResponseWriter, req *http.Request) {
    22  	hdrs := make(map[string][]string)
    23  
    24  	for n, v := range req.Header {
    25  		hdrs[n] = v
    26  	}
    27  
    28  	logger := cfg.logger.With(
    29  		zap.String("function", "renderHandler"),
    30  		zap.String("method", req.Method),
    31  		zap.String("path", req.URL.Path),
    32  		zap.Any("headers", hdrs),
    33  	)
    34  
    35  	logger.Info("got request")
    36  	if cfg.Code != http.StatusOK {
    37  		wr.WriteHeader(cfg.Code)
    38  		return
    39  	}
    40  
    41  	format, err := getFormat(req)
    42  	if err != nil {
    43  		logger.Error("bad request, failed to parse format")
    44  		wr.WriteHeader(http.StatusBadRequest)
    45  		_, _ = wr.Write([]byte(err.Error()))
    46  		return
    47  	}
    48  
    49  	targets := req.Form["target"]
    50  	maxDataPoints := int64(0)
    51  
    52  	if format == protoV3Format {
    53  		body, err := io.ReadAll(req.Body)
    54  		if err != nil {
    55  			logger.Error("bad request, failed to read request body",
    56  				zap.Error(err),
    57  			)
    58  			http.Error(wr, "bad request (failed to read request body): "+err.Error(), http.StatusBadRequest)
    59  			return
    60  		}
    61  
    62  		var pv3Request carbonapi_v3_pb.MultiFetchRequest
    63  		err = pv3Request.Unmarshal(body)
    64  
    65  		if err != nil {
    66  			logger.Error("bad request, failed to unmarshal request",
    67  				zap.Error(err),
    68  			)
    69  			http.Error(wr, "bad request (failed to parse format): "+err.Error(), http.StatusBadRequest)
    70  			return
    71  		}
    72  
    73  		targets = make([]string, len(pv3Request.Metrics))
    74  		for i, r := range pv3Request.Metrics {
    75  			targets[i] = r.PathExpression
    76  		}
    77  		maxDataPoints = pv3Request.Metrics[0].MaxDataPoints
    78  	}
    79  
    80  	logger.Info("request details",
    81  		zap.Strings("target", targets),
    82  		zap.String("format", format.String()),
    83  		zap.Int64("maxDataPoints", maxDataPoints),
    84  	)
    85  
    86  	multiv2 := carbonapi_v2_pb.MultiFetchResponse{
    87  		Metrics: []carbonapi_v2_pb.FetchResponse{},
    88  	}
    89  
    90  	multiv3 := carbonapi_v3_pb.MultiFetchResponse{
    91  		Metrics: []carbonapi_v3_pb.FetchResponse{},
    92  	}
    93  
    94  	httpCode := http.StatusOK
    95  	for _, target := range targets {
    96  		if response, ok := cfg.Expressions[target]; ok {
    97  			if response.ReplyDelayMS > 0 {
    98  				delay := time.Duration(response.ReplyDelayMS) * time.Millisecond
    99  				logger.Info("will add extra delay",
   100  					zap.Duration("delay", delay),
   101  				)
   102  				time.Sleep(delay)
   103  			}
   104  			if response.Code > 0 && response.Code != http.StatusOK {
   105  				httpCode = response.Code
   106  			}
   107  			if httpCode == http.StatusOK {
   108  				for _, m := range response.Data {
   109  					step := m.Step
   110  					if step == 0 {
   111  						step = 1
   112  					}
   113  					startTime := m.StartTime
   114  					if startTime == 0 {
   115  						startTime = step
   116  					}
   117  					isAbsent := make([]bool, 0, len(m.Values))
   118  					protov2Values := make([]float64, 0, len(m.Values))
   119  					for i := range m.Values {
   120  						if math.IsNaN(m.Values[i]) {
   121  							isAbsent = append(isAbsent, true)
   122  							protov2Values = append(protov2Values, 0.0)
   123  						} else {
   124  							isAbsent = append(isAbsent, false)
   125  							protov2Values = append(protov2Values, m.Values[i])
   126  						}
   127  					}
   128  					fr2 := carbonapi_v2_pb.FetchResponse{
   129  						Name:      m.MetricName,
   130  						StartTime: int32(startTime),
   131  						StopTime:  int32(startTime + step*len(protov2Values)),
   132  						StepTime:  int32(step),
   133  						Values:    protov2Values,
   134  						IsAbsent:  isAbsent,
   135  					}
   136  
   137  					fr3 := carbonapi_v3_pb.FetchResponse{
   138  						Name:                    m.MetricName,
   139  						PathExpression:          target,
   140  						ConsolidationFunc:       "avg",
   141  						StartTime:               int64(startTime),
   142  						StopTime:                int64(startTime + step*len(m.Values)),
   143  						StepTime:                int64(step),
   144  						XFilesFactor:            0,
   145  						HighPrecisionTimestamps: false,
   146  						Values:                  m.Values,
   147  						RequestStartTime:        1,
   148  						RequestStopTime:         int64(startTime + step*len(m.Values)),
   149  					}
   150  
   151  					multiv2.Metrics = append(multiv2.Metrics, fr2)
   152  					multiv3.Metrics = append(multiv3.Metrics, fr3)
   153  				}
   154  			}
   155  		}
   156  	}
   157  
   158  	if httpCode == http.StatusOK {
   159  		if len(multiv2.Metrics) == 0 {
   160  			wr.WriteHeader(http.StatusNotFound)
   161  			_, _ = wr.Write([]byte("Not found"))
   162  			return
   163  		}
   164  
   165  		if cfg.Listener.ShuffleResults {
   166  			rand.Shuffle(len(multiv2.Metrics), func(i, j int) {
   167  				multiv2.Metrics[i], multiv2.Metrics[j] = multiv2.Metrics[j], multiv2.Metrics[i]
   168  			})
   169  			rand.Shuffle(len(multiv3.Metrics), func(i, j int) {
   170  				multiv3.Metrics[i], multiv3.Metrics[j] = multiv3.Metrics[j], multiv3.Metrics[i]
   171  			})
   172  		}
   173  
   174  		contentType, d := cfg.marshalResponse(wr, logger, format, multiv3, multiv2)
   175  		if d == nil {
   176  			return
   177  		}
   178  		wr.Header().Set("Content-Type", contentType)
   179  		_, _ = wr.Write(d)
   180  	} else {
   181  		wr.WriteHeader(httpCode)
   182  		_, _ = wr.Write([]byte(http.StatusText(httpCode)))
   183  	}
   184  }
   185  
   186  func (cfg *listener) marshalResponse(wr http.ResponseWriter, logger *zap.Logger, format responseFormat, multiv3 carbonapi_v3_pb.MultiFetchResponse, multiv2 carbonapi_v2_pb.MultiFetchResponse) (string, []byte) {
   187  	var d []byte
   188  	var contentType string
   189  	var err error
   190  	switch format {
   191  	case pickleFormat:
   192  		contentType = httpHeaders.ContentTypePickle
   193  		if cfg.EmptyBody {
   194  			break
   195  		}
   196  		var response []map[string]interface{}
   197  
   198  		for _, metric := range multiv3.GetMetrics() {
   199  			m := make(map[string]interface{})
   200  			m["start"] = metric.StartTime
   201  			m["step"] = metric.StepTime
   202  			m["end"] = metric.StopTime
   203  			m["name"] = metric.Name
   204  			m["pathExpression"] = metric.PathExpression
   205  			m["xFilesFactor"] = 0.5
   206  			m["consolidationFunc"] = "avg"
   207  
   208  			mv := make([]interface{}, len(metric.Values))
   209  			for i, p := range metric.Values {
   210  				if math.IsNaN(p) {
   211  					mv[i] = nil
   212  				} else {
   213  					mv[i] = p
   214  				}
   215  			}
   216  
   217  			m["values"] = mv
   218  			log.Printf("%+v\n\n", m)
   219  			response = append(response, m)
   220  		}
   221  
   222  		var buf bytes.Buffer
   223  		logger.Info("request will be served",
   224  			zap.String("format", "pickle"),
   225  			zap.Any("content", response),
   226  		)
   227  		pEnc := ogórek.NewEncoder(&buf)
   228  		err = pEnc.Encode(response)
   229  		if err != nil {
   230  			wr.WriteHeader(http.StatusBadGateway)
   231  			_, _ = wr.Write([]byte(err.Error()))
   232  			return "", nil
   233  		}
   234  		d = buf.Bytes()
   235  	case protoV2Format:
   236  		contentType = httpHeaders.ContentTypeCarbonAPIv2PB
   237  		if cfg.EmptyBody {
   238  			break
   239  		}
   240  		logger.Info("request will be served",
   241  			zap.String("format", "protov2"),
   242  			zap.Any("content", multiv2),
   243  		)
   244  		d, err = multiv2.Marshal()
   245  		if err != nil {
   246  			wr.WriteHeader(http.StatusBadGateway)
   247  			_, _ = wr.Write([]byte(err.Error()))
   248  			return "", nil
   249  		}
   250  	case protoV3Format:
   251  		contentType = httpHeaders.ContentTypeCarbonAPIv3PB
   252  		if cfg.EmptyBody {
   253  			break
   254  		}
   255  		logger.Info("request will be served",
   256  			zap.String("format", "protov3"),
   257  			zap.Any("content", multiv3),
   258  		)
   259  		d, err = multiv3.Marshal()
   260  		if err != nil {
   261  			wr.WriteHeader(http.StatusBadGateway)
   262  			_, _ = wr.Write([]byte(err.Error()))
   263  			return "", nil
   264  		}
   265  	case jsonFormat:
   266  		contentType = "application/json"
   267  		if cfg.EmptyBody {
   268  			break
   269  		}
   270  		logger.Info("request will be served",
   271  			zap.String("format", "json"),
   272  			zap.Any("content", multiv2),
   273  		)
   274  		d, err = json.Marshal(multiv2)
   275  		if err != nil {
   276  			wr.WriteHeader(http.StatusBadGateway)
   277  			_, _ = wr.Write([]byte(err.Error()))
   278  			return "", nil
   279  		}
   280  	default:
   281  		logger.Error("format is not supported",
   282  			zap.Any("format", format),
   283  		)
   284  		return "", nil
   285  	}
   286  	return contentType, d
   287  }