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 }