github.com/grafana/synthetic-monitoring-agent@v0.64.2/pkg/collector/collector_test.go (about) 1 package collector_test 2 3 import ( 4 "context" 5 "strconv" 6 "strings" 7 "sync/atomic" 8 "testing" 9 "time" 10 11 "github.com/go-logfmt/logfmt" 12 "github.com/google/uuid" 13 "github.com/grafana/synthetic-monitoring-agent/pkg/collector" 14 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 15 "github.com/prometheus/client_golang/prometheus" 16 "github.com/stretchr/testify/require" 17 ) 18 19 type suppliedProbe struct { 20 eventTime time.Time 21 success bool 22 } 23 24 func (suppliedProbe) Name() string { return "supplied" } 25 26 func (p suppliedProbe) Probe(_ context.Context, _ string, registry *prometheus.Registry, logger collector.Logger) (bool, float64) { 27 gauge := prometheus.NewGauge(prometheus.GaugeOpts{Name: "probe_supplied_value"}) 28 registry.MustRegister(gauge) 29 gauge.Set(7) 30 31 _ = logger.Log( 32 "level", "INFO", 33 "msg", "supplied probe detail", 34 "time", p.eventTime.Format(time.RFC3339Nano), 35 ) 36 37 return p.success, 0.25 38 } 39 40 type untimedProbe struct{} 41 42 func (untimedProbe) Name() string { return "untimed" } 43 44 func (untimedProbe) Probe(_ context.Context, _ string, _ *prometheus.Registry, logger collector.Logger) (bool, float64) { 45 _ = logger.Log("level", "INFO", "msg", "untimed-probe-detail") 46 47 return true, 0.25 48 } 49 50 // trackedContext observes whether a child context detaches from its parent. 51 // context.WithCancel calls AfterFunc on a parent that implements it, then calls 52 // the returned stop function once the child is cancelled — an unexported detail 53 // of the standard library, so a future Go release could fail this test while 54 // Close is still correct. 55 type trackedContext struct { 56 context.Context 57 done chan struct{} 58 stopped atomic.Bool 59 } 60 61 func (c *trackedContext) Done() <-chan struct{} { return c.done } 62 63 func (c *trackedContext) AfterFunc(func()) func() bool { 64 return func() bool { 65 return !c.stopped.Swap(true) 66 } 67 } 68 69 func TestCloseReleasesCollectorContext(t *testing.T) { 70 ctx := &trackedContext{ 71 Context: context.Background(), 72 done: make(chan struct{}), 73 } 74 c, err := collector.New(ctx, testCheck(), testProbe(), untimedProbe{}) 75 require.NoError(t, err) 76 require.False(t, ctx.stopped.Load()) 77 78 c.Close() 79 c.Close() 80 81 require.True(t, ctx.stopped.Load()) 82 } 83 84 func TestCollectDefaultsToPrefixedUserLabels(t *testing.T) { 85 check := testCheck() 86 check.Labels = []sm.Label{{Name: "team", Value: "history"}} 87 c, err := collector.New(context.Background(), check, testProbe(), untimedProbe{}) 88 require.NoError(t, err) 89 90 defer c.Close() 91 92 series, _, err := c.Collect(context.Background(), time.Now()) 93 require.NoError(t, err) 94 95 labels := metricLabels(t, series, "sm_check_info") 96 require.Contains(t, labels, "label_team") 97 require.NotContains(t, labels, "team") 98 } 99 100 func TestCollectAllowsUnprefixedUserLabels(t *testing.T) { 101 check := testCheck() 102 check.Labels = []sm.Label{{Name: "team", Value: "history"}} 103 c, err := collector.New( 104 context.Background(), 105 check, 106 testProbe(), 107 untimedProbe{}, 108 collector.WithLabelMode(sm.LabelMode_LABEL_MODE_UNPREFIXED), 109 ) 110 require.NoError(t, err) 111 112 defer c.Close() 113 114 series, _, err := c.Collect(context.Background(), time.Now()) 115 require.NoError(t, err) 116 117 labels := metricLabels(t, series, "sm_check_info") 118 require.Contains(t, labels, "team") 119 require.NotContains(t, labels, "label_team") 120 } 121 122 func TestCollectUsesLogicalEventTimeAndAgentExecutionMetadata(t *testing.T) { 123 eventTime := time.Date(2020, 3, 1, 12, 0, 0, 0, time.UTC) 124 c, err := collector.New(context.Background(), testCheck(), testProbe(), suppliedProbe{ 125 eventTime: eventTime.Add(50 * time.Millisecond), 126 success: true, 127 }) 128 require.NoError(t, err) 129 130 defer c.Close() 131 132 series, streams, err := c.Collect(context.Background(), eventTime) 133 require.NoError(t, err) 134 require.NotEmpty(t, series) 135 require.Len(t, streams, 1) 136 137 foundMetric := false 138 139 for _, ts := range series { 140 for _, label := range ts.Labels { 141 if label.Name == "__name__" && label.Value == "probe_supplied_value" { 142 foundMetric = true 143 144 require.Len(t, ts.Samples, 1) 145 require.Equal(t, eventTime.UnixMilli(), ts.Samples[0].Timestamp) 146 } 147 } 148 } 149 150 require.True(t, foundMetric) 151 152 var ( 153 executionID string 154 walltime float64 155 ) 156 157 for _, entry := range streams[0].Entries { 158 require.Len(t, entry.StructuredMetadata, 1) 159 metadata := entry.StructuredMetadata[0] 160 require.Equal(t, "execution_id", metadata.Name) 161 162 if executionID == "" { 163 executionID = metadata.Value 164 } 165 166 require.Equal(t, executionID, metadata.Value) 167 168 if strings.Contains(entry.Line, "msg=\"Check succeeded\"") { 169 walltime = logfmtFloat(t, entry.Line, "walltime_seconds") 170 require.Equal(t, eventTime.Add(250*time.Millisecond), entry.Timestamp) 171 } 172 173 if strings.Contains(entry.Line, "msg=\"Beginning check\"") { 174 require.Equal(t, eventTime, entry.Timestamp, "beginning-check log must carry the logical event start time") 175 } 176 } 177 178 require.NoError(t, uuid.Validate(executionID)) 179 require.Positive(t, walltime) 180 require.NotEqual(t, 0.25, walltime, "walltime must measure execution, not logical event duration") 181 } 182 183 func TestCollectReturnsFailedProbeTelemetry(t *testing.T) { 184 eventTime := time.Date(2020, 3, 1, 12, 0, 0, 0, time.UTC) 185 c, err := collector.New(context.Background(), testCheck(), testProbe(), suppliedProbe{ 186 eventTime: eventTime, 187 success: false, 188 }) 189 require.NoError(t, err) 190 191 defer c.Close() 192 193 series, streams, err := c.Collect(context.Background(), eventTime) 194 require.Error(t, err) 195 require.NotEmpty(t, series) 196 require.NotEmpty(t, streams) 197 } 198 199 func TestCollectUsesLogicalEventTimeForUntimedProbeLogs(t *testing.T) { 200 eventTime := time.Date(2020, 3, 1, 12, 0, 0, 0, time.UTC) 201 c, err := collector.New(context.Background(), testCheck(), testProbe(), untimedProbe{}) 202 require.NoError(t, err) 203 204 defer c.Close() 205 206 _, streams, err := c.Collect(context.Background(), eventTime) 207 require.NoError(t, err) 208 require.Len(t, streams, 1) 209 210 foundUntimedLog := false 211 212 for _, entry := range streams[0].Entries { 213 if strings.Contains(entry.Line, "msg=untimed-probe-detail") { 214 foundUntimedLog = true 215 216 require.Equal(t, eventTime, entry.Timestamp, "probe logs without an explicit time must use the logical event time") 217 } 218 } 219 220 require.True(t, foundUntimedLog, "expected the injected probe log in collected streams") 221 } 222 223 func logfmtFloat(t *testing.T, line, wanted string) float64 { 224 t.Helper() 225 226 decoder := logfmt.NewDecoder(strings.NewReader(line)) 227 for decoder.ScanRecord() { 228 for decoder.ScanKeyval() { 229 if string(decoder.Key()) == wanted { 230 value, err := strconv.ParseFloat(string(decoder.Value()), 64) 231 require.NoError(t, err) 232 233 return value 234 } 235 } 236 } 237 238 require.NoError(t, decoder.Err()) 239 t.Fatalf("log field %q not found in %q", wanted, line) 240 241 return 0 242 } 243 244 func metricLabels(t *testing.T, series collector.TimeSeries, metricName string) map[string]string { 245 t.Helper() 246 247 for _, ts := range series { 248 labels := make(map[string]string, len(ts.Labels)) 249 for _, label := range ts.Labels { 250 labels[label.Name] = label.Value 251 } 252 253 if labels["__name__"] == metricName { 254 return labels 255 } 256 } 257 258 t.Fatalf("metric %q not found", metricName) 259 260 return nil 261 } 262 263 func testCheck() sm.Check { 264 return sm.Check{ 265 Id: 42, 266 TenantId: 1, 267 Target: "https://example.com", 268 Job: "collector-test", 269 Frequency: 60_000, 270 Timeout: 5_000, 271 Settings: sm.CheckSettings{ 272 Http: &sm.HttpSettings{IpVersion: sm.IpVersion_V4}, 273 }, 274 } 275 } 276 277 func testProbe() sm.Probe { 278 return sm.Probe{Name: "test-probe", Region: "test-region"} 279 }