github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/targets/heroku/target.go (about)

     1  package heroku
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/go-kit/log"
    10  	"github.com/go-kit/log/level"
    11  	herokuEncoding "github.com/heroku/x/logplex/encoding"
    12  	"github.com/prometheus/common/model"
    13  	"github.com/prometheus/prometheus/model/labels"
    14  	"github.com/prometheus/prometheus/model/relabel"
    15  	"github.com/weaveworks/common/logging"
    16  	"github.com/weaveworks/common/server"
    17  
    18  	"github.com/grafana/loki/clients/pkg/promtail/api"
    19  	lokiClient "github.com/grafana/loki/clients/pkg/promtail/client"
    20  	"github.com/grafana/loki/clients/pkg/promtail/scrapeconfig"
    21  	"github.com/grafana/loki/clients/pkg/promtail/targets/serverutils"
    22  	"github.com/grafana/loki/clients/pkg/promtail/targets/target"
    23  
    24  	"github.com/grafana/loki/pkg/logproto"
    25  	util_log "github.com/grafana/loki/pkg/util/log"
    26  )
    27  
    28  type Target struct {
    29  	logger         log.Logger
    30  	handler        api.EntryHandler
    31  	config         *scrapeconfig.HerokuDrainTargetConfig
    32  	jobName        string
    33  	server         *server.Server
    34  	metrics        *Metrics
    35  	relabelConfigs []*relabel.Config
    36  }
    37  
    38  // NewTarget creates a brand new Heroku Drain target, capable of receiving logs from a Heroku application through an HTTP drain.
    39  func NewTarget(metrics *Metrics, logger log.Logger, handler api.EntryHandler, jobName string, config *scrapeconfig.HerokuDrainTargetConfig, relabel []*relabel.Config) (*Target, error) {
    40  	wrappedLogger := log.With(logger, "component", "heroku_drain")
    41  
    42  	ht := &Target{
    43  		metrics:        metrics,
    44  		logger:         wrappedLogger,
    45  		handler:        handler,
    46  		jobName:        jobName,
    47  		config:         config,
    48  		relabelConfigs: relabel,
    49  	}
    50  
    51  	mergedServerConfigs, err := serverutils.MergeWithDefaults(config.Server)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("failed to parse configs and override defaults when configuring heroku drain target: %w", err)
    54  	}
    55  	// Set the config to the new combined config.
    56  	config.Server = mergedServerConfigs
    57  
    58  	err = ht.run()
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return ht, nil
    64  }
    65  
    66  func (h *Target) run() error {
    67  	level.Info(h.logger).Log("msg", "starting heroku drain target", "job", h.jobName)
    68  
    69  	// To prevent metric collisions because all metrics are going to be registered in the global Prometheus registry.
    70  
    71  	tentativeServerMetricNamespace := "promtail_heroku_drain_target_" + h.jobName
    72  	if !model.IsValidMetricName(model.LabelValue(tentativeServerMetricNamespace)) {
    73  		return fmt.Errorf("invalid prometheus-compatible job name: %s", h.jobName)
    74  	}
    75  	h.config.Server.MetricsNamespace = tentativeServerMetricNamespace
    76  
    77  	// We don't want the /debug and /metrics endpoints running, since this is not the main promtail HTTP server.
    78  	// We want this target to expose the least surface area possible, hence disabling WeaveWorks HTTP server metrics
    79  	// and debugging functionality.
    80  	h.config.Server.RegisterInstrumentation = false
    81  
    82  	// Wrapping util logger with component-specific key vals, and the expected GoKit logging interface
    83  	h.config.Server.Log = logging.GoKit(log.With(util_log.Logger, "component", "heroku_drain"))
    84  
    85  	srv, err := server.New(h.config.Server)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	h.server = srv
    91  	h.server.HTTP.Path("/heroku/api/v1/drain").Methods("POST").Handler(http.HandlerFunc(h.drain))
    92  
    93  	go func() {
    94  		err := srv.Run()
    95  		if err != nil {
    96  			level.Error(h.logger).Log("msg", "heroku drain target shutdown with error", "err", err)
    97  		}
    98  	}()
    99  
   100  	return nil
   101  }
   102  
   103  func (h *Target) drain(w http.ResponseWriter, r *http.Request) {
   104  	entries := h.handler.Chan()
   105  	defer r.Body.Close()
   106  	herokuScanner := herokuEncoding.NewDrainScanner(r.Body)
   107  	for herokuScanner.Scan() {
   108  		ts := time.Now()
   109  		message := herokuScanner.Message()
   110  		lb := labels.NewBuilder(nil)
   111  		lb.Set("__heroku_drain_host", message.Hostname)
   112  		lb.Set("__heroku_drain_app", message.Application)
   113  		lb.Set("__heroku_drain_proc", message.Process)
   114  		lb.Set("__heroku_drain_log_id", message.ID)
   115  
   116  		if h.config.UseIncomingTimestamp {
   117  			ts = message.Timestamp
   118  		}
   119  
   120  		// If the incoming request carries the tenant id, inject it as the reserved label so it's used by the
   121  		// remote write client.
   122  		tenantIDHeaderValue := r.Header.Get("X-Scope-OrgID")
   123  		if tenantIDHeaderValue != "" {
   124  			lb.Set(lokiClient.ReservedLabelTenantID, tenantIDHeaderValue)
   125  		}
   126  
   127  		processed := relabel.Process(lb.Labels(), h.relabelConfigs...)
   128  
   129  		// Start with the set of labels fixed in the configuration
   130  		filtered := h.Labels().Clone()
   131  		for _, lbl := range processed {
   132  			if strings.HasPrefix(lbl.Name, "__") && lbl.Name != lokiClient.ReservedLabelTenantID {
   133  				continue
   134  			}
   135  			filtered[model.LabelName(lbl.Name)] = model.LabelValue(lbl.Value)
   136  		}
   137  
   138  		entries <- api.Entry{
   139  			Labels: filtered,
   140  			Entry: logproto.Entry{
   141  				Timestamp: ts,
   142  				Line:      message.Message,
   143  			},
   144  		}
   145  		h.metrics.herokuEntries.WithLabelValues().Inc()
   146  	}
   147  	err := herokuScanner.Err()
   148  	if err != nil {
   149  		h.metrics.herokuErrors.WithLabelValues().Inc()
   150  		level.Warn(h.logger).Log("msg", "failed to read incoming heroku request", "err", err.Error())
   151  		http.Error(w, err.Error(), http.StatusBadRequest)
   152  		return
   153  	}
   154  	w.WriteHeader(http.StatusNoContent)
   155  }
   156  
   157  func (h *Target) Type() target.TargetType {
   158  	return target.HerokuDrainTargetType
   159  }
   160  
   161  func (h *Target) DiscoveredLabels() model.LabelSet {
   162  	return nil
   163  }
   164  
   165  func (h *Target) Labels() model.LabelSet {
   166  	return h.config.Labels
   167  }
   168  
   169  func (h *Target) Ready() bool {
   170  	return true
   171  }
   172  
   173  func (h *Target) Details() interface{} {
   174  	return map[string]string{}
   175  }
   176  
   177  func (h *Target) Stop() error {
   178  	level.Info(h.logger).Log("msg", "stopping heroku drain target", "job", h.jobName)
   179  	h.server.Shutdown()
   180  	h.handler.Stop()
   181  	return nil
   182  }