github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/pusher/v2/tenant_pusher.go (about) 1 package v2 2 3 import ( 4 "context" 5 "errors" 6 "math/rand" 7 "sync/atomic" 8 "time" 9 10 "github.com/gogo/protobuf/proto" 11 "github.com/golang/snappy" 12 "github.com/prometheus/client_golang/prometheus" 13 "github.com/prometheus/prometheus/prompb" 14 "golang.org/x/sync/errgroup" 15 16 logproto "github.com/grafana/loki/pkg/push" 17 "github.com/grafana/synthetic-monitoring-agent/internal/model" 18 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 19 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 20 ) 21 22 // tenantPusher is in charge of pushing changes for a specific tenant. 23 type tenantPusher struct { 24 tenantID model.GlobalID 25 pushCounter uint64 // FIXME(mem) 26 logs, metrics queue 27 tenantProvider pusher.TenantProvider 28 options pusherOptions 29 } 30 31 var _ payloadHandler = &tenantPusher{} 32 33 func newTenantPusher(tenantID model.GlobalID, tenantProvider pusher.TenantProvider, options pusherOptions) *tenantPusher { 34 mOptions := options.withType(pusher.LabelValueMetrics) 35 eOptions := options.withType(pusher.LabelValueLogs) 36 tp := &tenantPusher{ 37 tenantID: tenantID, 38 tenantProvider: tenantProvider, 39 options: options, 40 logs: newQueue(&eOptions), 41 metrics: newQueue(&mOptions), 42 } 43 44 return tp 45 } 46 47 func (p *tenantPusher) run(ctx context.Context) payloadHandler { 48 p.options.logger.Info().Msg("starting pusher") 49 err := p.runPushers(ctx) 50 p.options.logger.Info().Err(err).Msg("pusher terminated") 51 52 if err == nil || err == context.Canceled || err == errTenantIdle { 53 p.options.logger.Info().Err(err).Msg("clearing tenant slot") 54 return nil 55 } 56 57 var pErr pushError 58 if !errors.As(err, &pErr) { 59 p.options.logger.Warn().Err(err).Msg("unexpected error, clearing slot") 60 return nil 61 } 62 63 switch pErr.kind { 64 case errKindWait: 65 // Some failure forces us to stop pushing for a while, but keep accumulating metrics meanwhile. 66 p.options.logger.Info().Dur("delay", p.options.waitPeriod).Msg("delaying metrics publishing") 67 68 return &delayPusher{ 69 next: p, 70 delay: p.options.waitPeriod, 71 } 72 73 case errKindTenant: 74 p.options.logger.Debug().Msg("refreshing tenant") 75 // The tenant could be stale. Let's just restart the pusher with a minimal delay. 76 return &delayPusher{ 77 next: p, 78 delay: p.options.tenantDelay, 79 } 80 81 case errKindFatal: 82 // Possibly a situation where we can't push metrics and won't be able to push them 83 // in the future. Discard metrics for this tenant for an extended period of time. 84 p.options.logger.Warn().Dur("duration", p.options.discardPeriod).Msg("discarding metrics") 85 86 return &discardPusher{ 87 duration: p.options.discardPeriod, 88 options: p.options, 89 } 90 91 default: 92 p.options.logger.Warn().Err(err).Msg("unexpected error, clearing slot") 93 return nil 94 } 95 } 96 97 func (p *tenantPusher) runPushers(ctx context.Context) error { 98 // TODO(adrian): If tenant had the plan in here, we could have different retention for paid tenants. 99 tenant, err := p.tenantProvider.GetTenant(ctx, &sm.TenantInfo{ 100 Id: int64(p.tenantID), 101 }) 102 if err != nil { 103 p.options.metrics.FailedCounter.With(prometheus.Labels{"type": pusher.LabelValueMetrics, "reason": pusher.LabelValueTenant}).Inc() 104 p.options.metrics.FailedCounter.With(prometheus.Labels{"type": pusher.LabelValueLogs, "reason": pusher.LabelValueTenant}).Inc() 105 106 return pushError{ 107 kind: errKindFatal, 108 inner: err, 109 } 110 } 111 112 g, gCtx := errgroup.WithContext(ctx) 113 114 g.Go(func() error { 115 return p.metrics.push(gCtx, tenant.MetricsRemote) 116 }) 117 118 g.Go(func() error { 119 return p.logs.push(gCtx, tenant.EventsRemote) 120 }) 121 122 // Optionally perform some validations. 123 124 if p.options.maxIdleTime > 0 { 125 g.Go(idleChecker(gCtx, p.options.maxIdleTime, &p.pushCounter)) 126 } 127 128 if p.options.maxLifetime > 0 { 129 // generate a random number [-maxLifetimeJitter/2, maxLifetimeJitter/2) 130 jitter := (rand.Float64() - 0.5) * p.options.maxLifetimeJitter 131 // adjust maxLifetime by that amount 132 maxLifetime := p.options.maxLifetime + time.Duration(jitter*float64(p.options.maxLifetime)) 133 g.Go(maxLifetimeChecker(gCtx, maxLifetime)) 134 } 135 136 return g.Wait() 137 } 138 139 var ( 140 errMaxLifeTimeExceeded = pushError{ 141 kind: errKindTenant, 142 inner: errors.New("max life time exceeded"), 143 } 144 errTenantIdle = errors.New("tenant is idle") 145 ) 146 147 func idleChecker(ctx context.Context, interval time.Duration, ptr *uint64) func() error { 148 return func() error { 149 t := time.NewTicker(interval) 150 defer t.Stop() 151 152 lastValue := atomic.LoadUint64(ptr) 153 154 for { 155 select { 156 case <-ctx.Done(): 157 return ctx.Err() 158 case <-t.C: 159 // check that source has changed 160 curValue := atomic.LoadUint64(ptr) 161 if curValue == lastValue { 162 return errTenantIdle 163 } 164 165 lastValue = curValue 166 } 167 } 168 } 169 } 170 171 func maxLifetimeChecker(ctx context.Context, maxLifetime time.Duration) func() error { 172 return func() error { 173 t := time.NewTimer(maxLifetime) 174 select { 175 case <-ctx.Done(): 176 t.Stop() 177 return ctx.Err() 178 case <-t.C: 179 return errMaxLifeTimeExceeded 180 } 181 } 182 } 183 184 func (p *tenantPusher) publish(payload pusher.Payload) { 185 atomic.AddUint64(&p.pushCounter, 1) 186 187 if len(payload.Metrics()) > 0 { 188 p.metrics.insert(toRequest(&prompb.WriteRequest{Timeseries: payload.Metrics()}, p.options.pool)) 189 } 190 191 if len(payload.Streams()) > 0 { 192 p.logs.insert(toRequest(&logproto.PushRequest{Streams: payload.Streams()}, p.options.pool)) 193 } 194 } 195 196 func toRequest(m proto.Marshaler, p bufferPool) *[]byte { 197 data, err := m.Marshal() 198 if err != nil { 199 panic(err) 200 } 201 202 bufPtr := p.get() 203 *bufPtr = (*bufPtr)[0:cap(*bufPtr)] 204 encoded := snappy.Encode(*bufPtr, data) 205 206 return &encoded 207 } 208 209 type delayPusher struct { 210 next payloadHandler 211 delay time.Duration 212 } 213 214 var _ payloadHandler = delayPusher{} 215 216 func (p delayPusher) run(ctx context.Context) payloadHandler { 217 err := sleepCtx(ctx, p.delay) 218 if err != nil { 219 // Context cancelled 220 return nil 221 } 222 223 return p.next 224 } 225 226 func (p delayPusher) publish(payloads pusher.Payload) { 227 p.next.publish(payloads) 228 } 229 230 type discardPusher struct { 231 duration time.Duration 232 options pusherOptions 233 } 234 235 var _ payloadHandler = discardPusher{} 236 237 func (p discardPusher) run(ctx context.Context) payloadHandler { 238 // error can be ignored (only possible is context.Canceled) 239 // as it's returning a nil handler anyway. 240 _ = sleepCtx(ctx, p.duration) 241 return nil 242 } 243 244 func (p discardPusher) publish(payloads pusher.Payload) { 245 if len(payloads.Metrics()) > 0 { 246 p.options.metrics.DroppedCounter.WithLabelValues(pusher.LabelValueMetrics).Inc() 247 } 248 249 if len(payloads.Streams()) > 0 { 250 p.options.metrics.DroppedCounter.WithLabelValues(pusher.LabelValueLogs).Inc() 251 } 252 }