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

     1  package otlp
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"google.golang.org/grpc"
    13  	"google.golang.org/grpc/codes"
    14  	"google.golang.org/grpc/keepalive"
    15  	"google.golang.org/grpc/status"
    16  	"google.golang.org/protobuf/encoding/protojson"
    17  	"google.golang.org/protobuf/proto"
    18  
    19  	"github.com/dustin/go-humanize"
    20  	"github.com/go-kit/log"
    21  	"github.com/go-kit/log/level"
    22  	"github.com/google/uuid"
    23  	"github.com/grafana/dskit/server"
    24  	"github.com/grafana/dskit/tenant"
    25  	pprofileotlp "go.opentelemetry.io/proto/otlp/collector/profiles/v1development"
    26  	v1 "go.opentelemetry.io/proto/otlp/common/v1"
    27  
    28  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    29  	distributormodel "github.com/grafana/pyroscope/pkg/distributor/model"
    30  	"github.com/grafana/pyroscope/pkg/model"
    31  	"github.com/grafana/pyroscope/pkg/pprof"
    32  	httputil "github.com/grafana/pyroscope/pkg/util/http"
    33  	"github.com/grafana/pyroscope/pkg/validation"
    34  )
    35  
    36  type ingestHandler struct {
    37  	pprofileotlp.UnimplementedProfilesServiceServer
    38  	svc     PushService
    39  	log     log.Logger
    40  	handler http.Handler
    41  	limits  Limits
    42  }
    43  
    44  type Handler interface {
    45  	http.Handler
    46  	pprofileotlp.ProfilesServiceServer
    47  }
    48  
    49  type PushService interface {
    50  	PushBatch(ctx context.Context, req *distributormodel.PushRequest) error
    51  }
    52  
    53  type Limits interface {
    54  	IngestionBodyLimitBytes(tenantID string) int64
    55  }
    56  
    57  func NewOTLPIngestHandler(cfg server.Config, svc PushService, l log.Logger, limits Limits) Handler {
    58  	h := &ingestHandler{
    59  		svc:    svc,
    60  		log:    l,
    61  		limits: limits,
    62  	}
    63  
    64  	grpcServer := newGrpcServer(cfg)
    65  	pprofileotlp.RegisterProfilesServiceServer(grpcServer, h)
    66  
    67  	h.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    68  		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
    69  			grpcServer.ServeHTTP(w, r)
    70  			return
    71  		}
    72  
    73  		// Handle HTTP/JSON and HTTP/Protobuf requests
    74  		contentType := r.Header.Get("Content-Type")
    75  		if contentType == "application/json" || contentType == "application/x-protobuf" || contentType == "application/protobuf" {
    76  			h.handleHTTPRequest(w, r)
    77  			return
    78  		}
    79  
    80  		http.Error(w, fmt.Sprintf("Unsupported Content-Type: %s", contentType), http.StatusUnsupportedMediaType)
    81  	})
    82  
    83  	return h
    84  }
    85  
    86  func newGrpcServer(cfg server.Config) *grpc.Server {
    87  	grpcKeepAliveOptions := keepalive.ServerParameters{
    88  		MaxConnectionIdle:     cfg.GRPCServerMaxConnectionIdle,
    89  		MaxConnectionAge:      cfg.GRPCServerMaxConnectionAge,
    90  		MaxConnectionAgeGrace: cfg.GRPCServerMaxConnectionAgeGrace,
    91  		Time:                  cfg.GRPCServerTime,
    92  		Timeout:               cfg.GRPCServerTimeout,
    93  	}
    94  
    95  	grpcKeepAliveEnforcementPolicy := keepalive.EnforcementPolicy{
    96  		MinTime:             cfg.GRPCServerMinTimeBetweenPings,
    97  		PermitWithoutStream: cfg.GRPCServerPingWithoutStreamAllowed,
    98  	}
    99  
   100  	grpcOptions := []grpc.ServerOption{
   101  		grpc.KeepaliveParams(grpcKeepAliveOptions),
   102  		grpc.KeepaliveEnforcementPolicy(grpcKeepAliveEnforcementPolicy),
   103  		grpc.MaxRecvMsgSize(cfg.GRPCServerMaxRecvMsgSize),
   104  		grpc.MaxSendMsgSize(cfg.GRPCServerMaxSendMsgSize),
   105  		grpc.MaxConcurrentStreams(uint32(cfg.GRPCServerMaxConcurrentStreams)),
   106  		grpc.NumStreamWorkers(uint32(cfg.GRPCServerNumWorkers)),
   107  	}
   108  
   109  	grpcOptions = append(grpcOptions, cfg.GRPCOptions...)
   110  
   111  	return grpc.NewServer(grpcOptions...)
   112  }
   113  
   114  func (h *ingestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   115  	h.handler.ServeHTTP(w, r)
   116  }
   117  
   118  func isHTTPRequestBodyTooLarge(err error) error {
   119  	herr := new(http.MaxBytesError)
   120  	if errors.As(err, &herr) {
   121  		return validation.NewErrorf(validation.BodySizeLimit, "profile payload size exceeds limit of %s", humanize.Bytes(uint64(herr.Limit)))
   122  	}
   123  	return nil
   124  }
   125  
   126  func isKnownValidationError(err error) bool {
   127  	return validation.ReasonOf(err) != validation.Unknown
   128  }
   129  
   130  func (h *ingestHandler) handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
   131  	tenantID, err := tenant.TenantID(r.Context())
   132  	if err != nil {
   133  		httputil.ErrorWithStatus(w, err, http.StatusUnauthorized)
   134  		return
   135  	}
   136  	maxBodyBytes := h.limits.IngestionBodyLimitBytes(tenantID)
   137  
   138  	defer r.Body.Close()
   139  
   140  	var (
   141  		errMsgBodyRead = "failed to read request body"
   142  		reader         = r.Body
   143  	)
   144  
   145  	if strings.EqualFold(r.Header.Get("Content-Encoding"), "gzip") {
   146  		gzipReader, gzipErr := gzip.NewReader(r.Body)
   147  		if gzipErr != nil {
   148  			level.Error(h.log).Log("msg", "failed to create gzip reader", "err", gzipErr)
   149  			http.Error(w, "Failed to read request body", http.StatusBadRequest)
   150  			return
   151  		}
   152  		defer gzipReader.Close()
   153  		errMsgBodyRead = "failed to read gzip-compressed request body"
   154  
   155  		reader = gzipReader
   156  		// Limit after decompression size
   157  		if maxBodyBytes > 0 {
   158  			reader = io.NopCloser(io.LimitReader(reader, maxBodyBytes+1))
   159  		}
   160  	}
   161  
   162  	body, err := io.ReadAll(reader)
   163  	if maxBodyBytes > 0 && int64(len(body)) > maxBodyBytes {
   164  		validation.DiscardedBytes.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(float64(maxBodyBytes))
   165  		validation.DiscardedProfiles.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(1)
   166  		err := validation.NewErrorf(validation.BodySizeLimit, "uncompressed profile payload size exceeds limit of %s", humanize.Bytes(uint64(maxBodyBytes)))
   167  		http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
   168  		return
   169  	}
   170  	if err != nil {
   171  		level.Error(h.log).Log("msg", errMsgBodyRead, "err", err)
   172  		// handle if body size limit is hit with correct status code
   173  		if herr := isHTTPRequestBodyTooLarge(err); herr != nil {
   174  			validation.DiscardedBytes.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(float64(maxBodyBytes))
   175  			validation.DiscardedProfiles.WithLabelValues(string(validation.BodySizeLimit), tenantID).Add(1)
   176  			http.Error(w, herr.Error(), http.StatusRequestEntityTooLarge)
   177  			return
   178  		}
   179  		http.Error(w, errMsgBodyRead, http.StatusBadRequest)
   180  		return
   181  	}
   182  
   183  	req := &pprofileotlp.ExportProfilesServiceRequest{}
   184  
   185  	isJSONRequest := r.Header.Get("Content-Type") == "application/json"
   186  	if isJSONRequest {
   187  		if err := protojson.Unmarshal(body, req); err != nil {
   188  			level.Error(h.log).Log("msg", "failed to unmarshal JSON request", "err", err)
   189  			http.Error(w, "Failed to parse JSON request", http.StatusBadRequest)
   190  			return
   191  		}
   192  	} else {
   193  		if err := proto.Unmarshal(body, req); err != nil {
   194  			level.Error(h.log).Log("msg", "failed to unmarshal protobuf request", "err", err)
   195  			http.Error(w, "Failed to parse protobuf request", http.StatusBadRequest)
   196  			return
   197  		}
   198  	}
   199  
   200  	resp, err := h.export(r.Context(), req)
   201  	if err != nil {
   202  		level.Error(h.log).Log("msg", "failed to process profiles", "err", err)
   203  		if isKnownValidationError(err) {
   204  			http.Error(w, err.Error(), http.StatusBadRequest)
   205  		} else {
   206  			http.Error(w, err.Error(), http.StatusInternalServerError)
   207  		}
   208  		return
   209  	}
   210  
   211  	respBytes, err := proto.Marshal(resp)
   212  	if err != nil {
   213  		level.Error(h.log).Log("msg", "failed to marshal response", "err", err)
   214  		http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
   215  		return
   216  	}
   217  
   218  	w.Header().Set("Content-Type", "application/x-protobuf")
   219  	w.WriteHeader(http.StatusOK)
   220  	if _, err := w.Write(respBytes); err != nil {
   221  		level.Error(h.log).Log("msg", "failed to write response", "err", err)
   222  	}
   223  }
   224  
   225  func (h *ingestHandler) Export(ctx context.Context, er *pprofileotlp.ExportProfilesServiceRequest) (*pprofileotlp.ExportProfilesServiceResponse, error) {
   226  	return h.export(ctx, er)
   227  }
   228  
   229  func (h *ingestHandler) export(ctx context.Context, er *pprofileotlp.ExportProfilesServiceRequest) (*pprofileotlp.ExportProfilesServiceResponse, error) {
   230  	_, err := tenant.TenantID(ctx)
   231  	if err != nil {
   232  		return &pprofileotlp.ExportProfilesServiceResponse{}, status.Errorf(codes.Unauthenticated, "failed to extract tenant ID from context: %s", err.Error())
   233  	}
   234  
   235  	dc := er.Dictionary
   236  	if dc == nil {
   237  		return &pprofileotlp.ExportProfilesServiceResponse{}, status.Errorf(codes.InvalidArgument, "missing profile metadata dictionary")
   238  	}
   239  
   240  	rps := er.ResourceProfiles
   241  	if rps == nil {
   242  		return &pprofileotlp.ExportProfilesServiceResponse{}, status.Errorf(codes.InvalidArgument, "missing resource profiles")
   243  	}
   244  
   245  	for _, rp := range rps {
   246  		serviceName := getServiceNameFromAttributes(rp.Resource.GetAttributes())
   247  		for _, sp := range rp.ScopeProfiles {
   248  			for _, p := range sp.Profiles {
   249  				pprofProfiles, err := ConvertOtelToGoogle(p, dc)
   250  				if err != nil {
   251  					grpcError := status.Errorf(codes.InvalidArgument, "failed to convert otel profile: %s", err.Error())
   252  					return &pprofileotlp.ExportProfilesServiceResponse{}, grpcError
   253  				}
   254  
   255  				req := &distributormodel.PushRequest{
   256  					ReceivedCompressedProfileSize: proto.Size(p),
   257  					RawProfileType:                distributormodel.RawProfileTypeOTEL,
   258  				}
   259  
   260  				for samplesServiceName, pprofProfile := range pprofProfiles {
   261  					labels := getDefaultLabels()
   262  					labels = append(labels, pprofProfile.name)
   263  					processedKeys := map[string]bool{model.LabelNameProfileName: true}
   264  					labels = appendAttributesUnique(labels, rp.Resource.GetAttributes(), processedKeys)
   265  					labels = appendAttributesUnique(labels, sp.Scope.GetAttributes(), processedKeys)
   266  					svc := samplesServiceName
   267  					if svc == "" {
   268  						svc = serviceName
   269  					}
   270  					labels = append(labels, &typesv1.LabelPair{
   271  						Name:  model.LabelNameServiceName,
   272  						Value: svc,
   273  					})
   274  
   275  					s := &distributormodel.ProfileSeries{
   276  						Labels:     labels,
   277  						RawProfile: nil,
   278  						Profile:    pprof.RawFromProto(pprofProfile.profile),
   279  						ID:         uuid.New().String(),
   280  					}
   281  					req.Series = append(req.Series, s)
   282  				}
   283  				if len(req.Series) == 0 {
   284  					continue
   285  				}
   286  				err = h.svc.PushBatch(ctx, req)
   287  				if err != nil {
   288  					h.log.Log("msg", "failed to push profile", "err", err)
   289  					// Note: Validation metrics are already tracked by the distributor for errors
   290  					// returned from PushBatch, so we don't track them here to avoid double-counting.
   291  					return &pprofileotlp.ExportProfilesServiceResponse{}, fmt.Errorf("failed to make a GRPC request: %w", err)
   292  				}
   293  			}
   294  		}
   295  	}
   296  
   297  	return &pprofileotlp.ExportProfilesServiceResponse{}, nil
   298  }
   299  
   300  // getServiceNameFromAttributes extracts service name from OTLP resource attributes.
   301  // according to otel spec https://github.com/open-telemetry/opentelemetry-go/blob/ecfb73581f1b05af85fc393c3ce996a90cf2a5e2/semconv/v1.30.0/attribute_group.go#L10011-L10025
   302  // Returns "unknown_service:$process_name" if no service.name, but there is a process.executable.name
   303  // Returns "unknown_service" if no service.name and no process.executable.name
   304  func getServiceNameFromAttributes(attrs []*v1.KeyValue) string {
   305  	fallback := model.AttrServiceNameFallback
   306  	for _, attr := range attrs {
   307  		if attr.Key == string(model.AttrServiceName) {
   308  			val := attr.GetValue()
   309  			if sv := val.GetStringValue(); sv != "" {
   310  				return sv
   311  			}
   312  		}
   313  		if attr.Key == string(model.AttrProcessExecutableName) {
   314  			val := attr.GetValue()
   315  			if sv := val.GetStringValue(); sv != "" {
   316  				fallback += ":" + sv
   317  			}
   318  		}
   319  
   320  	}
   321  	return fallback
   322  }
   323  
   324  // getDefaultLabels returns the required base labels for Pyroscope profiles
   325  func getDefaultLabels() []*typesv1.LabelPair {
   326  	return []*typesv1.LabelPair{
   327  		{
   328  			Name:  model.LabelNameDelta,
   329  			Value: "false",
   330  		},
   331  		{
   332  			Name:  model.LabelNameOTEL,
   333  			Value: "true",
   334  		},
   335  	}
   336  }
   337  
   338  func appendAttributesUnique(labels []*typesv1.LabelPair, attrs []*v1.KeyValue, processedKeys map[string]bool) []*typesv1.LabelPair {
   339  	for _, attr := range attrs {
   340  		// Skip if we've already seen this key at any level
   341  		if processedKeys[attr.Key] {
   342  			continue
   343  		}
   344  
   345  		val := attr.GetValue()
   346  		if sv := val.GetStringValue(); sv != "" {
   347  			labels = append(labels, &typesv1.LabelPair{
   348  				Name:  attr.Key,
   349  				Value: sv,
   350  			})
   351  			processedKeys[attr.Key] = true
   352  		}
   353  	}
   354  	return labels
   355  }