github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/targets/heroku/target_test.go (about) 1 package heroku 2 3 import ( 4 "flag" 5 "fmt" 6 "net" 7 "net/http" 8 "os" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/go-kit/log" 14 "github.com/google/uuid" 15 "github.com/prometheus/client_golang/prometheus" 16 "github.com/prometheus/common/model" 17 "github.com/prometheus/prometheus/model/relabel" 18 "github.com/stretchr/testify/require" 19 "github.com/weaveworks/common/server" 20 21 lokiClient "github.com/grafana/loki/clients/pkg/promtail/client" 22 "github.com/grafana/loki/clients/pkg/promtail/client/fake" 23 "github.com/grafana/loki/clients/pkg/promtail/scrapeconfig" 24 ) 25 26 const localhost = "127.0.0.1" 27 28 const testPayload = `270 <158>1 2022-06-13T14:52:23.622778+00:00 host heroku router - at=info method=GET path="/" host=cryptic-cliffs-27764.herokuapp.com request_id=59da6323-2bc4-4143-8677-cc66ccfb115f fwd="181.167.87.140" dyno=web.1 connect=0ms service=3ms status=200 bytes=6979 protocol=https 29 ` 30 const testLogLine1 = `140 <190>1 2022-06-13T14:52:23.621815+00:00 host app web.1 - [GIN] 2022/06/13 - 14:52:23 | 200 | 1.428101ms | 181.167.87.140 | GET "/" 31 ` 32 const testLogLine1Timestamp = "2022-06-13T14:52:23.621815+00:00" 33 const testLogLine2 = `156 <190>1 2022-06-13T14:52:23.827271+00:00 host app web.1 - [GIN] 2022/06/13 - 14:52:23 | 200 | 163.92µs | 181.167.87.140 | GET "/static/main.css" 34 ` 35 36 func makeDrainRequest(host string, bodies ...string) (*http.Request, error) { 37 req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/heroku/api/v1/drain", host), strings.NewReader(strings.Join(bodies, ""))) 38 if err != nil { 39 return nil, err 40 } 41 42 drainToken := uuid.New().String() 43 frameID := uuid.New().String() 44 req.Header.Set("Content-Type", "application/heroku_drain-1") 45 req.Header.Set("Logplex-Drain-Token", fmt.Sprintf("d.%s", drainToken)) 46 req.Header.Set("Logplex-Frame-Id", frameID) 47 req.Header.Set("Logplex-Msg-Count", fmt.Sprintf("%d", len(bodies))) 48 49 return req, nil 50 } 51 52 func TestHerokuDrainTarget(t *testing.T) { 53 w := log.NewSyncWriter(os.Stderr) 54 logger := log.NewLogfmtLogger(w) 55 56 type expectedEntry struct { 57 labels model.LabelSet 58 line string 59 } 60 type args struct { 61 RequestBodies []string 62 RelabelConfigs []*relabel.Config 63 Labels model.LabelSet 64 } 65 66 cases := map[string]struct { 67 args args 68 expectedEntries []expectedEntry 69 }{ 70 "heroku request with a single log line, internal labels dropped, and fixed are propagated": { 71 args: args{ 72 RequestBodies: []string{testPayload}, 73 Labels: model.LabelSet{ 74 "job": "some_job_name", 75 }, 76 }, 77 expectedEntries: []expectedEntry{ 78 { 79 labels: model.LabelSet{ 80 "job": "some_job_name", 81 }, 82 line: `at=info method=GET path="/" host=cryptic-cliffs-27764.herokuapp.com request_id=59da6323-2bc4-4143-8677-cc66ccfb115f fwd="181.167.87.140" dyno=web.1 connect=0ms service=3ms status=200 bytes=6979 protocol=https 83 `, 84 }, 85 }, 86 }, 87 "heroku request with a two log lines, internal labels dropped, and fixed are propagated": { 88 args: args{ 89 RequestBodies: []string{testLogLine1, testLogLine2}, 90 Labels: model.LabelSet{ 91 "job": "multiple_line_job", 92 }, 93 }, 94 expectedEntries: []expectedEntry{ 95 { 96 labels: model.LabelSet{ 97 "job": "multiple_line_job", 98 }, 99 line: `[GIN] 2022/06/13 - 14:52:23 | 200 | 1.428101ms | 181.167.87.140 | GET "/" 100 `, 101 }, 102 { 103 labels: model.LabelSet{ 104 "job": "multiple_line_job", 105 }, 106 line: `[GIN] 2022/06/13 - 14:52:23 | 200 | 163.92µs | 181.167.87.140 | GET "/static/main.css" 107 `, 108 }, 109 }, 110 }, 111 "heroku request with a single log line, with internal labels relabeled, and fixed labels": { 112 args: args{ 113 RequestBodies: []string{testLogLine1}, 114 Labels: model.LabelSet{ 115 "job": "relabeling_job", 116 }, 117 RelabelConfigs: []*relabel.Config{ 118 { 119 SourceLabels: model.LabelNames{"__heroku_drain_host"}, 120 TargetLabel: "host", 121 Replacement: "$1", 122 Action: relabel.Replace, 123 Regex: relabel.MustNewRegexp("(.*)"), 124 }, 125 { 126 SourceLabels: model.LabelNames{"__heroku_drain_app"}, 127 TargetLabel: "app", 128 Replacement: "$1", 129 Action: relabel.Replace, 130 Regex: relabel.MustNewRegexp("(.*)"), 131 }, 132 { 133 SourceLabels: model.LabelNames{"__heroku_drain_proc"}, 134 TargetLabel: "procID", 135 Replacement: "$1", 136 Action: relabel.Replace, 137 Regex: relabel.MustNewRegexp("(.*)"), 138 }, 139 }, 140 }, 141 expectedEntries: []expectedEntry{ 142 { 143 line: `[GIN] 2022/06/13 - 14:52:23 | 200 | 1.428101ms | 181.167.87.140 | GET "/" 144 `, 145 labels: model.LabelSet{ 146 "host": "host", 147 "app": "app", 148 "procID": "web.1", 149 }, 150 }, 151 }, 152 }, 153 } 154 for name, tc := range cases { 155 t.Run(name, func(t *testing.T) { 156 // Create fake promtail client 157 eh := fake.New(func() {}) 158 defer eh.Stop() 159 160 serverConfig, port, err := getServerConfigWithAvailablePort() 161 require.NoError(t, err, "error generating server config or finding open port") 162 config := &scrapeconfig.HerokuDrainTargetConfig{ 163 Server: serverConfig, 164 Labels: tc.args.Labels, 165 UseIncomingTimestamp: false, 166 } 167 168 prometheus.DefaultRegisterer = prometheus.NewRegistry() 169 metrics := NewMetrics(prometheus.DefaultRegisterer) 170 pt, err := NewTarget(metrics, logger, eh, "test_job", config, tc.args.RelabelConfigs) 171 require.NoError(t, err) 172 defer func() { 173 _ = pt.Stop() 174 }() 175 176 // Clear received lines after test case is ran 177 defer eh.Clear() 178 179 // Send some logs 180 ts := time.Now() 181 182 req, err := makeDrainRequest(fmt.Sprintf("http://%s:%d", localhost, port), tc.args.RequestBodies...) 183 require.NoError(t, err, "expected test drain request to be successfully created") 184 res, err := http.DefaultClient.Do(req) 185 require.NoError(t, err) 186 require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code") 187 188 waitForMessages(eh) 189 190 // Make sure we didn't timeout 191 require.Equal(t, len(tc.args.RequestBodies), len(eh.Received())) 192 193 require.Equal(t, len(eh.Received()), len(tc.expectedEntries), "expected to receive equal amount of expected label sets") 194 for i, expectedEntry := range tc.expectedEntries { 195 // TODO: Add assertion over propagated timestamp 196 actualEntry := eh.Received()[i] 197 198 require.Equal(t, expectedEntry.line, actualEntry.Line, "expected line to be equal for %d-th entry", i) 199 200 expectedLS := expectedEntry.labels 201 actualLS := actualEntry.Labels 202 for label, value := range expectedLS { 203 require.Equal(t, expectedLS[label], actualLS[label], "expected label %s to be equal to %s in %d-th entry", label, value, i) 204 } 205 206 // Timestamp is always set in the handler, we expect received timestamps to be slightly higher than the timestamp when we started sending logs. 207 require.GreaterOrEqual(t, actualEntry.Timestamp.Unix(), ts.Unix(), "expected %d-th entry to have a received timestamp greater than publish time", i) 208 } 209 }) 210 } 211 } 212 213 func TestHerokuDrainTarget_UseIncomingTimestamp(t *testing.T) { 214 w := log.NewSyncWriter(os.Stderr) 215 logger := log.NewLogfmtLogger(w) 216 217 // Create fake promtail client 218 eh := fake.New(func() {}) 219 defer eh.Stop() 220 221 serverConfig, port, err := getServerConfigWithAvailablePort() 222 require.NoError(t, err, "error generating server config or finding open port") 223 config := &scrapeconfig.HerokuDrainTargetConfig{ 224 Server: serverConfig, 225 Labels: nil, 226 UseIncomingTimestamp: true, 227 } 228 229 prometheus.DefaultRegisterer = prometheus.NewRegistry() 230 metrics := NewMetrics(prometheus.DefaultRegisterer) 231 pt, err := NewTarget(metrics, logger, eh, "test_job", config, nil) 232 require.NoError(t, err) 233 defer func() { 234 _ = pt.Stop() 235 }() 236 237 // Clear received lines after test case is ran 238 defer eh.Clear() 239 240 req, err := makeDrainRequest(fmt.Sprintf("http://%s:%d", localhost, port), testLogLine1) 241 require.NoError(t, err, "expected test drain request to be successfully created") 242 res, err := http.DefaultClient.Do(req) 243 require.NoError(t, err) 244 require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code") 245 246 waitForMessages(eh) 247 248 // Make sure we didn't timeout 249 require.Equal(t, 1, len(eh.Received())) 250 251 expectedTs, err := time.Parse(time.RFC3339Nano, testLogLine1Timestamp) 252 require.NoError(t, err, "expected expected timestamp to be parse correctly") 253 require.Equal(t, expectedTs, eh.Received()[0].Timestamp, "expected entry timestamp to be overridden by received one") 254 } 255 256 func TestHerokuDrainTarget_ErrorOnNotPrometheusCompatibleJobName(t *testing.T) { 257 w := log.NewSyncWriter(os.Stderr) 258 logger := log.NewLogfmtLogger(w) 259 260 // Create fake promtail client 261 eh := fake.New(func() {}) 262 defer eh.Stop() 263 264 serverConfig, _, err := getServerConfigWithAvailablePort() 265 require.NoError(t, err, "error generating server config or finding open port") 266 config := &scrapeconfig.HerokuDrainTargetConfig{ 267 Server: serverConfig, 268 Labels: nil, 269 UseIncomingTimestamp: true, 270 } 271 272 prometheus.DefaultRegisterer = prometheus.NewRegistry() 273 metrics := NewMetrics(prometheus.DefaultRegisterer) 274 pt, err := NewTarget(metrics, logger, eh, "test-job", config, nil) 275 require.Error(t, err, "expected an error from creating a heroku target with an invalid job name") 276 // Cleanup target in the case test failed and target started correctly 277 if err == nil { 278 _ = pt.Stop() 279 } 280 } 281 282 func TestHerokuDrainTarget_UseTenantIDHeaderIfPresent(t *testing.T) { 283 w := log.NewSyncWriter(os.Stderr) 284 logger := log.NewLogfmtLogger(w) 285 286 // Create fake promtail client 287 eh := fake.New(func() {}) 288 defer eh.Stop() 289 290 serverConfig, port, err := getServerConfigWithAvailablePort() 291 require.NoError(t, err, "error generating server config or finding open port") 292 config := &scrapeconfig.HerokuDrainTargetConfig{ 293 Server: serverConfig, 294 Labels: nil, 295 UseIncomingTimestamp: true, 296 } 297 298 prometheus.DefaultRegisterer = prometheus.NewRegistry() 299 metrics := NewMetrics(prometheus.DefaultRegisterer) 300 pt, err := NewTarget(metrics, logger, eh, "test_job", config, nil) 301 require.NoError(t, err) 302 defer func() { 303 _ = pt.Stop() 304 }() 305 306 // Clear received lines after test case is ran 307 defer eh.Clear() 308 309 req, err := makeDrainRequest(fmt.Sprintf("http://%s:%d", localhost, port), testLogLine1) 310 require.NoError(t, err, "expected test drain request to be successfully created") 311 req.Header.Set("X-Scope-OrgID", "42") 312 res, err := http.DefaultClient.Do(req) 313 require.NoError(t, err) 314 require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code") 315 316 waitForMessages(eh) 317 318 // Make sure we didn't timeout 319 require.Equal(t, 1, len(eh.Received())) 320 321 require.Equal(t, model.LabelValue("42"), eh.Received()[0].Labels[lokiClient.ReservedLabelTenantID]) 322 } 323 324 func waitForMessages(eh *fake.Client) { 325 countdown := 1000 326 for len(eh.Received()) != 1 && countdown > 0 { 327 time.Sleep(1 * time.Millisecond) 328 countdown-- 329 } 330 } 331 332 func getServerConfigWithAvailablePort() (cfg server.Config, port int, err error) { 333 // Get a randomly available port by open and closing a TCP socket 334 addr, err := net.ResolveTCPAddr("tcp", localhost+":0") 335 if err != nil { 336 return 337 } 338 l, err := net.ListenTCP("tcp", addr) 339 if err != nil { 340 return 341 } 342 port = l.Addr().(*net.TCPAddr).Port 343 err = l.Close() 344 if err != nil { 345 return 346 } 347 348 // Adjust some of the defaults 349 cfg.RegisterFlags(flag.NewFlagSet("empty", flag.ContinueOnError)) 350 cfg.HTTPListenAddress = localhost 351 cfg.HTTPListenPort = port 352 cfg.GRPCListenAddress = localhost 353 cfg.GRPCListenPort = 0 // Not testing GRPC, a random port will be assigned 354 355 return 356 }