github.com/grafana/pyroscope@v1.18.0/pkg/ingester/pyroscope/ingest_handler.go (about)

     1  package pyroscope
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/opentracing/opentracing-go"
    15  	otlog "github.com/opentracing/opentracing-go/log"
    16  
    17  	"github.com/grafana/pyroscope/pkg/tenant"
    18  	httputil "github.com/grafana/pyroscope/pkg/util/http"
    19  	"github.com/grafana/pyroscope/pkg/validation"
    20  
    21  	"github.com/grafana/pyroscope/pkg/og/convert/speedscope"
    22  
    23  	"github.com/go-kit/log"
    24  	"github.com/go-kit/log/level"
    25  
    26  	"github.com/grafana/pyroscope/api/model/labelset"
    27  	"github.com/grafana/pyroscope/pkg/og/agent/types"
    28  	"github.com/grafana/pyroscope/pkg/og/convert/jfr"
    29  	"github.com/grafana/pyroscope/pkg/og/convert/pprof"
    30  	"github.com/grafana/pyroscope/pkg/og/convert/profile"
    31  	"github.com/grafana/pyroscope/pkg/og/ingestion"
    32  	"github.com/grafana/pyroscope/pkg/og/storage/metadata"
    33  	"github.com/grafana/pyroscope/pkg/og/util/attime"
    34  )
    35  
    36  // Copy-pasted from
    37  // https://github.com/grafana/pyroscope/blob/main/pkg/server/ingest.go
    38  // with minor changes to make it propagate http response codes.
    39  type ingestHandler struct {
    40  	log      log.Logger
    41  	ingester ingestion.Ingester
    42  }
    43  
    44  func NewIngestHandler(l log.Logger, p ingestion.Ingester) http.Handler {
    45  	return ingestHandler{
    46  		log:      level.Error(l),
    47  		ingester: p,
    48  	}
    49  }
    50  
    51  func (h ingestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    52  	ctx := r.Context()
    53  	sp, ctx := opentracing.StartSpanFromContext(ctx, "ingestHandler.ServeHTTP")
    54  	defer sp.Finish()
    55  
    56  	tenantID, _ := tenant.ExtractTenantIDFromContext(ctx)
    57  	sp.SetTag("tenant_id", tenantID)
    58  	input, err := h.parseInputMetadataFromRequest(ctx, r)
    59  	if err != nil {
    60  		msg := "failed to parse request metadata"
    61  		sp.LogFields(otlog.Error(err), otlog.String("msg", msg))
    62  		_ = h.log.Log("msg", msg, "err", err, "orgID", tenantID)
    63  		httputil.ErrorWithStatus(w, err, http.StatusBadRequest)
    64  		return
    65  	}
    66  
    67  	if err := readInputRawDataFromRequest(ctx, r, input); err != nil {
    68  		var status int
    69  		var maxBytesError *http.MaxBytesError
    70  		switch {
    71  		case errors.As(err, &maxBytesError):
    72  			err = fmt.Errorf("request body too large: %w", err)
    73  			status = http.StatusRequestEntityTooLarge
    74  			validation.DiscardedBytes.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(float64(maxBytesError.Limit))
    75  			validation.DiscardedProfiles.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(float64(1))
    76  		default:
    77  			status = http.StatusRequestTimeout
    78  		}
    79  
    80  		msg := "failed to read request body"
    81  		sp.LogFields(otlog.Error(err), otlog.String("msg", msg))
    82  		_ = h.log.Log("msg", msg, "err", err, "orgID", tenantID)
    83  		httputil.ErrorWithStatus(w, err, status)
    84  		return
    85  	}
    86  
    87  	err = h.ingester.Ingest(ctx, input)
    88  	if err != nil {
    89  		if ingestion.IsIngestionError(err) {
    90  			msg := "failed to convert profile"
    91  			sp.LogFields(otlog.Error(err), otlog.String("msg", msg))
    92  			_ = h.log.Log("msg", msg, "err", err, "orgID", tenantID)
    93  			httputil.Error(w, err)
    94  		} else {
    95  			msg := "failed to ingest profile"
    96  			sp.LogFields(otlog.Error(err), otlog.String("msg", msg))
    97  			httputil.ErrorWithStatus(w, err, http.StatusUnprocessableEntity)
    98  		}
    99  	}
   100  }
   101  
   102  func (h ingestHandler) parseInputMetadataFromRequest(_ context.Context, r *http.Request) (*ingestion.IngestInput, error) {
   103  	var (
   104  		q     = r.URL.Query()
   105  		input ingestion.IngestInput
   106  		err   error
   107  	)
   108  
   109  	input.Metadata.LabelSet, err = labelset.Parse(q.Get("name"))
   110  	if err != nil {
   111  		return nil, fmt.Errorf("name: %w", err)
   112  	}
   113  
   114  	if qt := q.Get("from"); qt != "" {
   115  		input.Metadata.StartTime = attime.Parse(qt)
   116  	} else {
   117  		input.Metadata.StartTime = time.Now()
   118  	}
   119  
   120  	if qt := q.Get("until"); qt != "" {
   121  		input.Metadata.EndTime = attime.Parse(qt)
   122  	} else {
   123  		input.Metadata.EndTime = time.Now()
   124  	}
   125  
   126  	if sr := q.Get("sampleRate"); sr != "" {
   127  		sampleRate, err := strconv.Atoi(sr)
   128  		if err != nil {
   129  			_ = h.log.Log(
   130  				"err", err,
   131  				"msg", fmt.Sprintf("invalid sample rate: %q", sr),
   132  			)
   133  			input.Metadata.SampleRate = types.DefaultSampleRate
   134  		} else {
   135  			input.Metadata.SampleRate = uint32(sampleRate)
   136  		}
   137  	} else {
   138  		input.Metadata.SampleRate = types.DefaultSampleRate
   139  	}
   140  
   141  	if sn := q.Get("spyName"); sn != "" {
   142  		// TODO: error handling
   143  		input.Metadata.SpyName = sn
   144  	} else {
   145  		input.Metadata.SpyName = "unknown"
   146  	}
   147  
   148  	if u := q.Get("units"); u != "" {
   149  		// TODO(petethepig): add validation for these?
   150  		input.Metadata.Units = metadata.Units(u)
   151  	} else {
   152  		input.Metadata.Units = metadata.SamplesUnits
   153  	}
   154  
   155  	if at := q.Get("aggregationType"); at != "" {
   156  		// TODO(petethepig): add validation for these?
   157  		input.Metadata.AggregationType = metadata.AggregationType(at)
   158  	} else {
   159  		input.Metadata.AggregationType = metadata.SumAggregationType
   160  	}
   161  
   162  	return &input, nil
   163  }
   164  
   165  func readInputRawDataFromRequest(ctx context.Context, r *http.Request, input *ingestion.IngestInput) error {
   166  	var (
   167  		sp          = opentracing.SpanFromContext(ctx)
   168  		format      = r.URL.Query().Get("format")
   169  		contentType = r.Header.Get("Content-Type")
   170  	)
   171  	if sp != nil {
   172  		sp.SetTag("format", format)
   173  		sp.SetTag("content_type", contentType)
   174  	}
   175  
   176  	buf := bytes.NewBuffer(make([]byte, 0, 64<<10))
   177  	n, err := io.Copy(buf, r.Body)
   178  	if err != nil {
   179  		return fmt.Errorf("error reading request body bytes_read %d: %w", n, err)
   180  	}
   181  
   182  	if sp != nil {
   183  		sp.SetTag("content_length", n)
   184  	}
   185  	b := buf.Bytes()
   186  
   187  	switch {
   188  	default:
   189  		input.Format = ingestion.FormatGroups
   190  	case format == "trie", contentType == "binary/octet-stream+trie":
   191  		input.Format = ingestion.FormatTrie
   192  	case format == "tree", contentType == "binary/octet-stream+tree":
   193  		input.Format = ingestion.FormatTree
   194  	case format == "lines":
   195  		input.Format = ingestion.FormatLines
   196  
   197  	case format == "jfr":
   198  		input.Format = ingestion.FormatJFR
   199  		input.Profile = &jfr.RawProfile{
   200  			FormDataContentType: contentType,
   201  			RawData:             b,
   202  		}
   203  
   204  	case format == "pprof":
   205  		input.Format = ingestion.FormatPprof
   206  		input.Profile = &pprof.RawProfile{
   207  			RawData: b,
   208  		}
   209  
   210  	case format == "speedscope":
   211  		input.Format = ingestion.FormatSpeedscope
   212  		input.Profile = &speedscope.RawProfile{
   213  			RawData: b,
   214  		}
   215  
   216  	case strings.Contains(contentType, "multipart/form-data"):
   217  		input.Profile = &pprof.RawProfile{
   218  			FormDataContentType: contentType,
   219  			RawData:             b,
   220  		}
   221  	}
   222  
   223  	if input.Profile == nil {
   224  		input.Profile = &profile.RawProfile{
   225  			Format:  input.Format,
   226  			RawData: b,
   227  		}
   228  	}
   229  	return nil
   230  }