github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/telemetry/region_pusher.go (about) 1 package telemetry 2 3 import ( 4 "context" 5 "fmt" 6 "slices" 7 "strings" 8 "sync" 9 "time" 10 11 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 12 13 prom "github.com/prometheus/client_golang/prometheus" 14 "github.com/rs/zerolog" 15 ) 16 17 const CalNilStringTerminator = "__MISSING__" 18 19 // RegionPusher periodically sends telemetry data for a specific region. 20 type RegionPusher struct { 21 client sm.TelemetryClient 22 logger zerolog.Logger 23 24 instance string 25 regionID int32 26 27 telemetry map[int64]map[sm.CheckClass]map[string]*sm.CheckClassTelemetry // Indexed by local tenant ID 28 telemetryMu sync.Mutex 29 30 metrics RegionMetrics 31 } 32 33 type RegionMetrics struct { 34 pushRequestsActive prom.Gauge 35 pushRequestsDuration prom.Observer 36 pushRequestsTotal prom.Counter 37 pushRequestsError prom.Counter 38 39 addExecutionDuration prom.Observer 40 } 41 42 // start handles region metrics before a push telemetry request. 43 func (m *RegionMetrics) start() (start time.Time) { 44 m.pushRequestsActive.Inc() 45 m.pushRequestsTotal.Inc() 46 47 return time.Now() 48 } 49 50 // end handles region metrics after a push telemetry request. 51 func (m *RegionMetrics) end(err error, start time.Time) { 52 m.pushRequestsActive.Dec() 53 m.pushRequestsDuration.Observe(time.Since(start).Seconds()) 54 55 if err != nil { 56 m.pushRequestsError.Inc() 57 } 58 } 59 60 // NewRegionPusher builds a new RegionPusher. 61 // Notice that the effective time span used to dictate the pace for periodic 62 // push events will be defined based on the given time span plus a random 63 // jitter [0,59)s. 64 func NewRegionPusher( 65 ctx context.Context, timeSpan time.Duration, 66 client sm.TelemetryClient, logger zerolog.Logger, instance string, regionID int32, 67 metrics RegionMetrics, options ...RegionPusherOption, 68 ) *RegionPusher { 69 tp := &RegionPusher{ 70 client: client, 71 logger: logger, 72 instance: instance, 73 regionID: regionID, 74 telemetry: make(map[int64]map[sm.CheckClass]map[string]*sm.CheckClassTelemetry), 75 metrics: metrics, 76 } 77 78 var opts regionPusherOptions 79 80 for _, opt := range options { 81 opt(&opts) 82 } 83 84 opts.ApplyDefaults(timeSpan) 85 86 opts.wg.Add(1) 87 88 go func() { 89 defer opts.wg.Done() 90 91 tp.run(ctx, opts.ticker, opts.wg) 92 }() 93 94 return tp 95 } 96 97 type regionPusherOptions struct { 98 ticker ticker 99 wg *sync.WaitGroup 100 } 101 102 func (opts *regionPusherOptions) ApplyDefaults(timeSpan time.Duration) { 103 if opts.ticker == nil { 104 opts.ticker = newStdTicker(timeSpan) 105 } 106 107 if opts.wg == nil { 108 opts.wg = new(sync.WaitGroup) 109 } 110 } 111 112 type RegionPusherOption func(opts *regionPusherOptions) 113 114 func WithTicker(t ticker) RegionPusherOption { 115 return func(opts *regionPusherOptions) { 116 opts.ticker = t 117 } 118 } 119 120 func WithWaitGroup(wg *sync.WaitGroup) RegionPusherOption { 121 return func(opts *regionPusherOptions) { 122 opts.wg = wg 123 } 124 } 125 126 func (p *RegionPusher) run(ctx context.Context, ticker ticker, wg *sync.WaitGroup) { 127 p.logger.Info().Msg("region pusher starting") 128 defer p.logger.Debug().Msg("region pusher stopped") 129 130 // TODO: We could potentially create here a pushCtx, pass that to push, and 131 // from push keep retrying sending the data if it fails initially until the 132 // pushCtx is canceled, which should happen once the ticker ticks again, to 133 // avoid overlapping push requests. 134 // By now, only retry folllowing the ticker's pace. 135 136 LOOP: 137 for { 138 select { 139 case <-ticker.C(): 140 p.logger.Info().Msg("pushing telemetry") 141 142 m := p.next() 143 144 wg.Add(1) 145 // Avoid blocking 146 go func() { 147 defer wg.Done() 148 149 p.push(m) 150 }() 151 152 case <-ctx.Done(): 153 p.logger.Debug().Msg("region pusher stopping") 154 155 m := p.next() 156 p.push(m) 157 ticker.Stop() 158 159 break LOOP 160 } 161 } 162 } 163 164 // AddExecution adds a new execution to the tenant telemetry. 165 func (p *RegionPusher) AddExecution(e Execution) { 166 start := time.Now() 167 168 p.telemetryMu.Lock() 169 defer p.telemetryMu.Unlock() 170 171 tenantTele, ok := p.telemetry[e.LocalTenantID] 172 if !ok { 173 tenantTele = make(map[sm.CheckClass]map[string]*sm.CheckClassTelemetry) 174 p.telemetry[e.LocalTenantID] = tenantTele 175 } 176 177 clTele, ok := tenantTele[e.CheckClass] 178 if !ok { 179 clTele = make(map[string]*sm.CheckClassTelemetry) 180 tenantTele[e.CheckClass] = clTele 181 } 182 183 calsKey := serializeCALs(e.CostAttributionLabels) 184 185 calTele, ok := clTele[calsKey] 186 if !ok { 187 // deserializeCals returns sorted labels and handles nil -> empty slice 188 calTele = &sm.CheckClassTelemetry{ 189 CheckClass: e.CheckClass, 190 CostAttributionLabels: deserializeCals(calsKey), 191 } 192 clTele[calsKey] = calTele 193 } 194 195 calTele.Executions++ 196 calTele.Duration += float32(e.Duration.Seconds()) 197 calTele.SampledExecutions += int32((e.Duration + time.Minute - 1) / time.Minute) 198 199 // measure contention for AddExecution 200 p.metrics.addExecutionDuration.Observe( 201 time.Since(start).Seconds(), 202 ) 203 } 204 205 func (p *RegionPusher) next() sm.RegionTelemetry { 206 m := sm.RegionTelemetry{ 207 Instance: p.instance, 208 RegionId: p.regionID, 209 Telemetry: make([]*sm.TenantTelemetry, 0, len(p.telemetry)), 210 } 211 212 p.telemetryMu.Lock() 213 defer p.telemetryMu.Unlock() 214 215 // Copy current telemetry data 216 for tenantID, tTele := range p.telemetry { 217 tenantTele := &sm.TenantTelemetry{ 218 TenantId: tenantID, 219 Telemetry: make([]*sm.CheckClassTelemetry, 0, len(tTele)), 220 } 221 for _, clTele := range tTele { 222 for _, calTele := range clTele { 223 tenantTele.Telemetry = append(tenantTele.Telemetry, &sm.CheckClassTelemetry{ 224 CheckClass: calTele.CheckClass, 225 Executions: calTele.Executions, 226 Duration: calTele.Duration, 227 SampledExecutions: calTele.SampledExecutions, 228 CostAttributionLabels: calTele.CostAttributionLabels, 229 }) 230 } 231 } 232 233 m.Telemetry = append(m.Telemetry, tenantTele) 234 } 235 236 return m 237 } 238 239 func serializeCALs(cals []sm.CostAttributionLabel) string { 240 if len(cals) == 0 || cals == nil { 241 return CalNilStringTerminator 242 } 243 244 sorted := make([]sm.CostAttributionLabel, len(cals)) 245 copy(sorted, cals) 246 slices.SortFunc(sorted, func(a, b sm.CostAttributionLabel) int { 247 return strings.Compare(a.Name, b.Name) 248 }) 249 250 parts := make([]string, len(sorted)) 251 for i, label := range sorted { 252 parts[i] = fmt.Sprintf("%s=%s", label.Name, label.Value) 253 } 254 255 return strings.Join(parts, ",") 256 } 257 258 func deserializeCals(calKey string) []sm.CostAttributionLabel { 259 if calKey == CalNilStringTerminator { 260 return []sm.CostAttributionLabel{} 261 } 262 263 split := strings.Split(calKey, ",") 264 265 cals := make([]sm.CostAttributionLabel, len(split)) 266 for i, cal := range split { 267 c := strings.Split(cal, "=") 268 cals[i] = sm.CostAttributionLabel{ 269 Name: c[0], 270 Value: c[1], 271 } 272 } 273 274 return cals 275 } 276 277 func (p *RegionPusher) push(m sm.RegionTelemetry) { 278 var ( 279 r *sm.PushTelemetryResponse 280 err error 281 ) 282 283 start := p.metrics.start() 284 285 defer func() { p.metrics.end(err, start) }() 286 287 // We don't want to cancel a possibly ongoing request even if the agent 288 // context is done, therefore use background context 289 ctx := context.Background() 290 291 r, err = p.client.PushTelemetry(ctx, &m) 292 if err != nil { 293 p.logger.Err(err).Msg("error pushing telemetry") 294 return 295 } 296 297 if r.Status.Code != sm.StatusCode_OK { 298 // create an error so it's handled by metrics.end() on defer 299 err = fmt.Errorf("unexpected status code") 300 p.logger.Err(err). 301 Int32("statusCode", int32(r.Status.Code)). 302 Str("statusMessage", r.Status.Message). 303 Msg("error pushing telemetry") 304 } 305 }