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

     1  package gcplog_test
     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/prometheus/client_golang/prometheus"
    15  	"github.com/prometheus/common/model"
    16  	"github.com/prometheus/prometheus/model/relabel"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/weaveworks/common/server"
    19  
    20  	lokiClient "github.com/grafana/loki/clients/pkg/promtail/client"
    21  	"github.com/grafana/loki/clients/pkg/promtail/client/fake"
    22  	"github.com/grafana/loki/clients/pkg/promtail/scrapeconfig"
    23  	"github.com/grafana/loki/clients/pkg/promtail/targets/gcplog"
    24  )
    25  
    26  const localhost = "127.0.0.1"
    27  
    28  const expectedMessageData = `{
    29    "severity": "DEBUG",
    30    "textPayload": "Function execution took 1198 ms. Finished with status code: 200",
    31    "trace": "projects/wired-height/traces/54aa8a20875253c6b47caa8bd28ad652"
    32  }`
    33  const testPayload = `
    34  {
    35  	"message": {
    36  		"attributes": {
    37  			"logging.googleapis.com/timestamp": "2022-07-25T22:19:09.903683708Z"
    38  		},
    39  		"data": "ewogICJzZXZlcml0eSI6ICJERUJVRyIsCiAgInRleHRQYXlsb2FkIjogIkZ1bmN0aW9uIGV4ZWN1dGlvbiB0b29rIDExOTggbXMuIEZpbmlzaGVkIHdpdGggc3RhdHVzIGNvZGU6IDIwMCIsCiAgInRyYWNlIjogInByb2plY3RzL3dpcmVkLWhlaWdodC90cmFjZXMvNTRhYThhMjA4NzUyNTNjNmI0N2NhYThiZDI4YWQ2NTIiCn0=",
    40  		"messageId": "5187581549398349",
    41  		"message_id": "5187581549398349",
    42  		"publishTime": "2022-07-25T22:19:15.56Z",
    43  		"publish_time": "2022-07-25T22:19:15.56Z"
    44  	},
    45  	"subscription": "projects/wired-height-350515/subscriptions/test"
    46  }`
    47  
    48  func makeGCPPushRequest(host string, body string) (*http.Request, error) {
    49  	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/gcp/api/v1/push", host), strings.NewReader(body))
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	return req, nil
    54  }
    55  
    56  func TestPushTarget(t *testing.T) {
    57  	w := log.NewSyncWriter(os.Stderr)
    58  	logger := log.NewLogfmtLogger(w)
    59  
    60  	type expectedEntry struct {
    61  		labels model.LabelSet
    62  		line   string
    63  	}
    64  	type args struct {
    65  		RequestBody    string
    66  		RelabelConfigs []*relabel.Config
    67  		Labels         model.LabelSet
    68  	}
    69  
    70  	cases := map[string]struct {
    71  		args            args
    72  		expectedEntries []expectedEntry
    73  	}{
    74  		"simplified cloud functions log line": {
    75  			args: args{
    76  				RequestBody: testPayload,
    77  				Labels: model.LabelSet{
    78  					"job": "some_job_name",
    79  				},
    80  			},
    81  			expectedEntries: []expectedEntry{
    82  				{
    83  					labels: model.LabelSet{
    84  						"job": "some_job_name",
    85  					},
    86  					line: expectedMessageData,
    87  				},
    88  			},
    89  		},
    90  		"simplified cloud functions log line, with relabeling custom attribute and message id": {
    91  			args: args{
    92  				RequestBody: testPayload,
    93  				Labels: model.LabelSet{
    94  					"job": "some_job_name",
    95  				},
    96  				RelabelConfigs: []*relabel.Config{
    97  					{
    98  						SourceLabels: model.LabelNames{"__gcp_attributes_logging_googleapis_com_timestamp"},
    99  						Regex:        relabel.MustNewRegexp("(.*)"),
   100  						Replacement:  "$1",
   101  						TargetLabel:  "google_timestamp",
   102  						Action:       relabel.Replace,
   103  					},
   104  					{
   105  						SourceLabels: model.LabelNames{"__gcp_message_id"},
   106  						Regex:        relabel.MustNewRegexp("(.*)"),
   107  						Replacement:  "$1",
   108  						TargetLabel:  "message_id",
   109  						Action:       relabel.Replace,
   110  					},
   111  				},
   112  			},
   113  			expectedEntries: []expectedEntry{
   114  				{
   115  					labels: model.LabelSet{
   116  						"job":              "some_job_name",
   117  						"google_timestamp": "2022-07-25T22:19:09.903683708Z",
   118  						"message_id":       "5187581549398349",
   119  					},
   120  					line: expectedMessageData,
   121  				},
   122  			},
   123  		},
   124  	}
   125  	for name, tc := range cases {
   126  		t.Run(name, func(t *testing.T) {
   127  			// Create fake promtail client
   128  			eh := fake.New(func() {})
   129  			defer eh.Stop()
   130  
   131  			serverConfig, port, err := getServerConfigWithAvailablePort()
   132  			require.NoError(t, err, "error generating server config or finding open port")
   133  			config := &scrapeconfig.GcplogTargetConfig{
   134  				Server:               serverConfig,
   135  				Labels:               tc.args.Labels,
   136  				UseIncomingTimestamp: false,
   137  				SubscriptionType:     "push",
   138  			}
   139  
   140  			prometheus.DefaultRegisterer = prometheus.NewRegistry()
   141  			metrics := gcplog.NewMetrics(prometheus.DefaultRegisterer)
   142  			pt, err := gcplog.NewGCPLogTarget(metrics, logger, eh, tc.args.RelabelConfigs, "test_job", config)
   143  			require.NoError(t, err)
   144  			defer func() {
   145  				_ = pt.Stop()
   146  			}()
   147  
   148  			// Clear received lines after test case is ran
   149  			defer eh.Clear()
   150  
   151  			// Send some logs
   152  			ts := time.Now()
   153  
   154  			req, err := makeGCPPushRequest(fmt.Sprintf("http://%s:%d", localhost, port), tc.args.RequestBody)
   155  			require.NoError(t, err, "expected test drain request to be successfully created")
   156  			res, err := http.DefaultClient.Do(req)
   157  			require.NoError(t, err)
   158  			require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code")
   159  
   160  			waitForMessages(eh)
   161  
   162  			// Make sure we didn't timeout
   163  			require.Equal(t, 1, len(eh.Received()))
   164  
   165  			require.Equal(t, len(eh.Received()), len(tc.expectedEntries), "expected to receive equal amount of expected label sets")
   166  			for i, expectedEntry := range tc.expectedEntries {
   167  				// TODO: Add assertion over propagated timestamp
   168  				actualEntry := eh.Received()[i]
   169  
   170  				require.Equal(t, expectedEntry.line, actualEntry.Line, "expected line to be equal for %d-th entry", i)
   171  
   172  				expectedLS := expectedEntry.labels
   173  				actualLS := actualEntry.Labels
   174  				for label, value := range expectedLS {
   175  					require.Equal(t, expectedLS[label], actualLS[label], "expected label %s to be equal to %s in %d-th entry", label, value, i)
   176  				}
   177  
   178  				// Timestamp is always set in the handler, we expect received timestamps to be slightly higher than the timestamp when we started sending logs.
   179  				require.GreaterOrEqual(t, actualEntry.Timestamp.Unix(), ts.Unix(), "expected %d-th entry to have a received timestamp greater than publish time", i)
   180  			}
   181  		})
   182  	}
   183  }
   184  
   185  func TestPushTarget_UseIncomingTimestamp(t *testing.T) {
   186  	w := log.NewSyncWriter(os.Stderr)
   187  	logger := log.NewLogfmtLogger(w)
   188  
   189  	// Create fake promtail client
   190  	eh := fake.New(func() {})
   191  	defer eh.Stop()
   192  
   193  	serverConfig, port, err := getServerConfigWithAvailablePort()
   194  	require.NoError(t, err, "error generating server config or finding open port")
   195  	config := &scrapeconfig.GcplogTargetConfig{
   196  		Server:               serverConfig,
   197  		Labels:               nil,
   198  		UseIncomingTimestamp: true,
   199  		SubscriptionType:     "push",
   200  	}
   201  
   202  	prometheus.DefaultRegisterer = prometheus.NewRegistry()
   203  	metrics := gcplog.NewMetrics(prometheus.DefaultRegisterer)
   204  	pt, err := gcplog.NewGCPLogTarget(metrics, logger, eh, nil, "test_job", config)
   205  	require.NoError(t, err)
   206  	defer func() {
   207  		_ = pt.Stop()
   208  	}()
   209  
   210  	// Clear received lines after test case is ran
   211  	defer eh.Clear()
   212  
   213  	req, err := makeGCPPushRequest(fmt.Sprintf("http://%s:%d", localhost, port), testPayload)
   214  	require.NoError(t, err, "expected test drain request to be successfully created")
   215  	res, err := http.DefaultClient.Do(req)
   216  	require.NoError(t, err)
   217  	require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code")
   218  
   219  	waitForMessages(eh)
   220  
   221  	// Make sure we didn't timeout
   222  	require.Equal(t, 1, len(eh.Received()))
   223  
   224  	expectedTs, err := time.Parse(time.RFC3339Nano, "2022-07-25T22:19:15.56Z")
   225  	require.NoError(t, err, "expected expected timestamp to be parse correctly")
   226  	require.Equal(t, expectedTs, eh.Received()[0].Timestamp, "expected entry timestamp to be overridden by received one")
   227  }
   228  
   229  func TestPushTarget_UseTenantIDHeaderIfPresent(t *testing.T) {
   230  	w := log.NewSyncWriter(os.Stderr)
   231  	logger := log.NewLogfmtLogger(w)
   232  
   233  	// Create fake promtail client
   234  	eh := fake.New(func() {})
   235  	defer eh.Stop()
   236  
   237  	serverConfig, port, err := getServerConfigWithAvailablePort()
   238  	require.NoError(t, err, "error generating server config or finding open port")
   239  	config := &scrapeconfig.GcplogTargetConfig{
   240  		Server:               serverConfig,
   241  		Labels:               nil,
   242  		UseIncomingTimestamp: true,
   243  		SubscriptionType:     "push",
   244  	}
   245  
   246  	prometheus.DefaultRegisterer = prometheus.NewRegistry()
   247  	metrics := gcplog.NewMetrics(prometheus.DefaultRegisterer)
   248  	pt, err := gcplog.NewGCPLogTarget(metrics, logger, eh, nil, "test_job", config)
   249  	require.NoError(t, err)
   250  	defer func() {
   251  		_ = pt.Stop()
   252  	}()
   253  
   254  	// Clear received lines after test case is ran
   255  	defer eh.Clear()
   256  
   257  	req, err := makeGCPPushRequest(fmt.Sprintf("http://%s:%d", localhost, port), testPayload)
   258  	require.NoError(t, err, "expected test drain request to be successfully created")
   259  	req.Header.Set("X-Scope-OrgID", "42")
   260  	res, err := http.DefaultClient.Do(req)
   261  	require.NoError(t, err)
   262  	require.Equal(t, http.StatusNoContent, res.StatusCode, "expected no-content status code")
   263  
   264  	waitForMessages(eh)
   265  
   266  	// Make sure we didn't timeout
   267  	require.Equal(t, 1, len(eh.Received()))
   268  
   269  	require.Equal(t, model.LabelValue("42"), eh.Received()[0].Labels[lokiClient.ReservedLabelTenantID])
   270  }
   271  
   272  func waitForMessages(eh *fake.Client) {
   273  	countdown := 1000
   274  	for len(eh.Received()) != 1 && countdown > 0 {
   275  		time.Sleep(1 * time.Millisecond)
   276  		countdown--
   277  	}
   278  }
   279  
   280  func getServerConfigWithAvailablePort() (cfg server.Config, port int, err error) {
   281  	// Get a randomly available port by open and closing a TCP socket
   282  	addr, err := net.ResolveTCPAddr("tcp", localhost+":0")
   283  	if err != nil {
   284  		return
   285  	}
   286  	l, err := net.ListenTCP("tcp", addr)
   287  	if err != nil {
   288  		return
   289  	}
   290  	port = l.Addr().(*net.TCPAddr).Port
   291  	err = l.Close()
   292  	if err != nil {
   293  		return
   294  	}
   295  
   296  	// Adjust some of the defaults
   297  	cfg.RegisterFlags(flag.NewFlagSet("empty", flag.ContinueOnError))
   298  	cfg.HTTPListenAddress = localhost
   299  	cfg.HTTPListenPort = port
   300  	cfg.GRPCListenAddress = localhost
   301  	cfg.GRPCListenPort = 0 // Not testing GRPC, a random port will be assigned
   302  
   303  	return
   304  }