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 }