recorder implementations
This commit is contained in:
		| @@ -10,7 +10,6 @@ import ( | ||||
| 	"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" | ||||
|  | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather/recorder/recorders" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather/recorder/recorders/memory" | ||||
| ) | ||||
|  | ||||
| type WeatherRecorder struct { | ||||
| @@ -27,13 +26,13 @@ type Opts struct { | ||||
| 	KeepLast int | ||||
| } | ||||
|  | ||||
| func NewWeatherRecorder(opts *Opts) *WeatherRecorder { | ||||
| func MustNewWeatherRecorder(opts *Opts) *WeatherRecorder { | ||||
| 	if opts.KeepLast < 1 { | ||||
| 		opts.KeepLast = 1 | ||||
| 	} | ||||
|  | ||||
| 	if opts.Recorder == nil { | ||||
| 		opts.Recorder = &memory.MemoryRecorder{} | ||||
| 		panic("no recorder provided") | ||||
| 	} | ||||
|  | ||||
| 	opts.Recorder.Init(opts.Ctx, &recorders.RecorderOpts{ | ||||
|   | ||||
							
								
								
									
										24
									
								
								pkg/weather/recorder/recorders/memory/count.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/weather/recorder/recorders/memory/count.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| package memory | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| ) | ||||
|  | ||||
| func (r *MemoryRecorder) Count(ctx context.Context) int { | ||||
| 	_, span := r.tracer.Start(ctx, "countWeatherRecorder") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	count := r.count() | ||||
|  | ||||
| 	span.SetAttributes(attribute.Int("count", count)) | ||||
| 	span.SetStatus(codes.Ok, "") | ||||
|  | ||||
| 	return count | ||||
| } | ||||
|  | ||||
| func (r *MemoryRecorder) count() int { | ||||
| 	return len(r.updates) | ||||
| } | ||||
| @@ -3,7 +3,6 @@ package memory | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"slices" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| @@ -16,7 +15,7 @@ import ( | ||||
| func (r *MemoryRecorder) Get(ctx context.Context, req *pb.GetWeatherRequest) ( | ||||
| 	[]*weather.WeatherUpdate, error, | ||||
| ) { | ||||
| 	ctx, span := r.tracer.Start(ctx, "memoryRecorder.Get") | ||||
| 	_, span := r.tracer.Start(ctx, "memoryRecorder.Get") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	r.RLock() | ||||
| @@ -24,11 +23,12 @@ func (r *MemoryRecorder) Get(ctx context.Context, req *pb.GetWeatherRequest) ( | ||||
|  | ||||
| 	span.AddEvent("acquired lock on recorder cache") | ||||
|  | ||||
| 	limit := util.GetLimitFromReq(req) | ||||
| 	if r.count() == 0 { | ||||
| 		err := errors.New("no recorded updates to get") | ||||
| 		span.RecordError(err) | ||||
| 		return nil, err | ||||
| 	} else if r.count() <= int(*req.Limit) { | ||||
| 	} else if limit > 0 && r.count() <= limit { | ||||
| 		span.RecordError(errors.New("requested more updates than recorded")) | ||||
| 	} | ||||
|  | ||||
| @@ -43,66 +43,8 @@ func (r *MemoryRecorder) Get(ctx context.Context, req *pb.GetWeatherRequest) ( | ||||
|  | ||||
| func (r *MemoryRecorder) getUpdatesFromReq(req *pb.GetWeatherRequest) []*weather.WeatherUpdate { | ||||
| 	if req.Opts == nil { | ||||
| 		return limitUpdates(r.updates, int(req.GetLimit())) | ||||
| 		return util.LimitUpdates(r.updates, util.GetLimitFromReq(req)) | ||||
| 	} | ||||
|  | ||||
| 	return r.applyOptsToUpdates(r.updates, int(req.GetLimit()), req.Opts) | ||||
| } | ||||
|  | ||||
| func (r *MemoryRecorder) applyOptsToUpdates(updates []*weather.WeatherUpdate, limit int, opts *pb.GetWeatherOpts) []*weather.WeatherUpdate { | ||||
| 	if opts == nil { | ||||
| 		return updates | ||||
| 	} else if opts.StationName == nil && opts.StationType == nil { | ||||
| 		return updates | ||||
| 	} | ||||
|  | ||||
| 	filtered := make([]*weather.WeatherUpdate, 0, limit) | ||||
|  | ||||
| 	for i := len(updates) - 1; i >= 0; i-- { | ||||
| 		update := updates[i] | ||||
| 		match := true | ||||
|  | ||||
| 		if opts.GetStationName() != "" { | ||||
| 			if update.GetStationName() != opts.GetStationName() { | ||||
| 				match = false | ||||
| 			} | ||||
| 		} | ||||
| 		if opts.GetStationType() != "" { | ||||
| 			if util.DerefStr(update.StationType) != opts.GetStationType() { | ||||
| 				match = false | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if match { | ||||
| 			filtered = append(filtered, update) | ||||
| 			if len(filtered) >= limit { | ||||
| 				return filtered | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return slices.Clip(filtered) | ||||
| } | ||||
|  | ||||
| func (r *MemoryRecorder) Count(ctx context.Context) int { | ||||
| 	_, span := r.tracer.Start(ctx, "countWeatherRecorder") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	count := r.count() | ||||
|  | ||||
| 	span.SetAttributes(attribute.Int("count", count)) | ||||
| 	span.SetStatus(codes.Ok, "") | ||||
|  | ||||
| 	return count | ||||
| } | ||||
|  | ||||
| func (r *MemoryRecorder) count() int { | ||||
| 	return len(r.updates) | ||||
| } | ||||
|  | ||||
| func limitUpdates(updates []*weather.WeatherUpdate, limit int) []*weather.WeatherUpdate { | ||||
| 	if len(updates) > limit { | ||||
| 		return updates[len(updates)-limit:] | ||||
| 	} | ||||
| 	return updates | ||||
| 	return util.ApplyOptsToUpdates(r.updates, util.GetLimitFromReq(req), req.Opts) | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								pkg/weather/recorder/recorders/noop/noop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								pkg/weather/recorder/recorders/noop/noop.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package noop | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather/recorder/recorders" | ||||
| ) | ||||
|  | ||||
| type NoopRecorder struct{} | ||||
|  | ||||
| func (n *NoopRecorder) Init(context.Context, *recorders.RecorderOpts) {} | ||||
|  | ||||
| func (n *NoopRecorder) Set(context.Context, *weather.WeatherUpdate) error { return nil } | ||||
|  | ||||
| func (n *NoopRecorder) Get(context.Context, *pb.GetWeatherRequest) ([]*weather.WeatherUpdate, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (n *NoopRecorder) Count(context.Context) int { return 0 } | ||||
| @@ -4,12 +4,14 @@ import ( | ||||
| 	"context" | ||||
|  | ||||
| 	pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/ambient/config" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather" | ||||
| ) | ||||
|  | ||||
| type RecorderOpts struct { | ||||
| 	RetainLast int | ||||
| 	BaseCtx    context.Context | ||||
| 	AppConfig  *config.AmbientLocalExporterConfig | ||||
| } | ||||
|  | ||||
| type Recorder interface { | ||||
|   | ||||
							
								
								
									
										38
									
								
								pkg/weather/recorder/recorders/redis/count.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								pkg/weather/recorder/recorders/redis/count.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
| ) | ||||
|  | ||||
| func (r *RedisRecorder) Count(ctx context.Context) int { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.count") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	r.RLock() | ||||
| 	defer r.RUnlock() | ||||
|  | ||||
| 	return r.count(ctx) | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) count(ctx context.Context) int { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.count.redis", trace.WithAttributes( | ||||
| 		attribute.String("updatesKey", r.Key()))) | ||||
| 	defer span.End() | ||||
|  | ||||
| 	count, err := r.redis.LLen(ctx, r.Key()).Result() | ||||
| 	if err != nil { | ||||
| 		span.RecordError(err) | ||||
| 		span.SetStatus(codes.Error, err.Error()) | ||||
| 		r.log.Err(err).Send() | ||||
| 		return int(count) | ||||
| 	} | ||||
|  | ||||
| 	span.SetAttributes(attribute.Int64("updatesCount", count)) | ||||
| 	span.SetStatus(codes.Ok, "") | ||||
|  | ||||
| 	return int(count) | ||||
| } | ||||
							
								
								
									
										96
									
								
								pkg/weather/recorder/recorders/redis/get.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								pkg/weather/recorder/recorders/redis/get.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"slices" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
|  | ||||
| 	pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/util" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather" | ||||
| ) | ||||
|  | ||||
| func (r *RedisRecorder) Get(ctx context.Context, req *pb.GetWeatherRequest) ( | ||||
| 	[]*weather.WeatherUpdate, error, | ||||
| ) { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.get", trace.WithAttributes( | ||||
| 		attribute.Int("limit", util.GetLimitFromReq(req)), | ||||
| 	)) | ||||
| 	defer span.End() | ||||
|  | ||||
| 	return r.get(ctx, req) | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) get(ctx context.Context, req *pb.GetWeatherRequest) ( | ||||
| 	[]*weather.WeatherUpdate, error, | ||||
| ) { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.get.redis") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	limit := util.GetLimitFromReq(req) | ||||
| 	if limit < 1 { | ||||
| 		limit = r.keep | ||||
| 	} | ||||
|  | ||||
| 	span.SetAttributes(attribute.Int("limit", limit)) | ||||
|  | ||||
| 	datas, err := r.redis.LRange(ctx, r.Key(), 0, int64(limit)).Result() | ||||
| 	if err != nil { | ||||
| 		span.RecordError(err) | ||||
| 		span.SetStatus(codes.Error, err.Error()) | ||||
| 		r.log.Err(err).Send() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	span.AddEvent("redis queried") | ||||
|  | ||||
| 	updates, err := jsonDatasToUpdates(datas) | ||||
| 	if err != nil { | ||||
| 		span.RecordError(err) | ||||
| 		span.SetStatus(codes.Error, err.Error()) | ||||
| 		r.log.Err(err).Send() | ||||
| 	} else { | ||||
| 		span.SetStatus(codes.Ok, "") | ||||
| 	} | ||||
|  | ||||
| 	span.AddEvent("results unmarshalled") | ||||
| 	span.SetAttributes(attribute.Int("results", len(updates))) | ||||
|  | ||||
| 	filtered := util.ApplyOptsToUpdates(updates, limit, req.Opts) | ||||
|  | ||||
| 	span.AddEvent("results filtered") | ||||
| 	span.SetAttributes( | ||||
| 		attribute.Int("filteredResults", len(filtered)), | ||||
| 		attribute.Int("resultsFiltered", len(updates)-len(filtered)), | ||||
| 	) | ||||
|  | ||||
| 	r.log.Debug(). | ||||
| 		Int("updatesRetrieved", len(updates)). | ||||
| 		Int("updatesAfterFiltering", len(filtered)). | ||||
| 		Int("updatesFiltered", len(updates)-len(filtered)). | ||||
| 		Msg("updates retrieved from redis") | ||||
|  | ||||
| 	return updates, err | ||||
| } | ||||
|  | ||||
| func jsonDatasToUpdates(datas []string) ([]*weather.WeatherUpdate, error) { | ||||
| 	var errs error | ||||
| 	updates := make([]*weather.WeatherUpdate, 0, len(datas)) | ||||
|  | ||||
| 	for _, data := range datas { | ||||
| 		update := new(weather.WeatherUpdate) | ||||
| 		err := json.Unmarshal([]byte(data), update) | ||||
| 		errs = errors.Join(errs, err) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			updates = append(updates, update) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return slices.Clip(updates), errs | ||||
| } | ||||
							
								
								
									
										108
									
								
								pkg/weather/recorder/recorders/redis/redis.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								pkg/weather/recorder/recorders/redis/redis.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"sync" | ||||
|  | ||||
| 	"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
|  | ||||
| 	redis "github.com/redis/go-redis/v9" | ||||
| 	"github.com/rs/zerolog" | ||||
|  | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/ambient/config" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/util" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather/recorder/recorders" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	DEF_RETAIN  = 120 | ||||
| 	UPDATES_KEY = "weatherUpdates" | ||||
| ) | ||||
|  | ||||
| type RedisRecorder struct { | ||||
| 	baseCtx context.Context | ||||
| 	tracer  trace.Tracer | ||||
| 	redis   *redis.Client | ||||
| 	config  *config.AmbientLocalExporterConfig | ||||
| 	log     *zerolog.Logger | ||||
| 	appKey  string // prefix for redis keys, uses app name, environment, and version | ||||
| 	keep    int | ||||
| 	*sync.RWMutex | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) Init(ctx context.Context, opts *recorders.RecorderOpts) { | ||||
| 	if opts.RetainLast < 1 { | ||||
| 		opts.RetainLast = DEF_RETAIN | ||||
| 	} | ||||
|  | ||||
| 	r.log = zerolog.Ctx(opts.BaseCtx) | ||||
|  | ||||
| 	r.tracer = otel.GetTracer(r.baseCtx, "redisRecorder") | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.init", trace.WithAttributes( | ||||
| 		attribute.String("redisHost", opts.AppConfig.RecorderConfig.RedisConfig.RedisHost), | ||||
| 		attribute.Int("retainLast", opts.RetainLast), | ||||
| 		attribute.Int("redisPort", opts.AppConfig.RecorderConfig.RedisConfig.RedisPort), | ||||
| 		attribute.Bool("tls", opts.AppConfig.RecorderConfig.RedisConfig.RedisTLS), | ||||
| 	)) | ||||
| 	defer span.End() | ||||
|  | ||||
| 	r.config = opts.AppConfig | ||||
| 	r.keep = opts.RetainLast | ||||
| 	r.baseCtx = opts.BaseCtx | ||||
| 	r.RWMutex = &sync.RWMutex{} | ||||
|  | ||||
| 	// Unique key prefix for this version/env/name of exporter | ||||
| 	// will be consistent across replicas, but resets on upgrade | ||||
| 	// as it is using version | ||||
| 	r.appKey = util.GetAppHash(r.config.AppConfig) | ||||
|  | ||||
| 	r.MustInitRedis(ctx) | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) MustInitRedis(ctx context.Context) { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.init.redis") | ||||
| 	defer span.End() | ||||
|  | ||||
| 	rc := r.config.RecorderConfig.RedisConfig | ||||
|  | ||||
| 	var tlsConfig *tls.Config | ||||
| 	if rc.RedisTLS { | ||||
| 		tlsConfig = &tls.Config{ | ||||
| 			ServerName:         rc.RedisHost, | ||||
| 			InsecureSkipVerify: rc.RedisTLSInsecure, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	r.redis = redis.NewClient(&redis.Options{ | ||||
| 		Addr:       fmt.Sprintf("%s:%d", rc.RedisHost, rc.RedisPort), | ||||
| 		ClientName: fmt.Sprintf("%s-%s", r.config.Name, r.config.Environment), | ||||
| 		Username:   rc.RedisUser, | ||||
| 		Password:   rc.RedisPassword, | ||||
| 		DB:         rc.RedisDB, | ||||
| 		TLSConfig:  tlsConfig, | ||||
| 	}) | ||||
|  | ||||
| 	span.AddEvent("redis client ready") | ||||
|  | ||||
| 	resp := r.redis.Ping(ctx) | ||||
| 	if resp.Err() != nil { | ||||
| 		span.RecordError(resp.Err()) | ||||
| 		span.SetStatus(codes.Error, resp.Err().Error()) | ||||
| 		r.log.Fatal().Err(resp.Err()).Msg("failed to ping redis") | ||||
| 	} | ||||
|  | ||||
| 	span.AddEvent("redis client ping ok") | ||||
| 	span.SetStatus(codes.Ok, "") | ||||
|  | ||||
| 	r.log.Info().Str("appKey", r.appKey). | ||||
| 		Msg("redis ping ok, client ready") | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) Key() string { | ||||
| 	return fmt.Sprintf("%s:%s", r.appKey, UPDATES_KEY) | ||||
| } | ||||
							
								
								
									
										61
									
								
								pkg/weather/recorder/recorders/redis/set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								pkg/weather/recorder/recorders/redis/set.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/attribute" | ||||
| 	"go.opentelemetry.io/otel/codes" | ||||
| 	"go.opentelemetry.io/otel/trace" | ||||
|  | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/util" | ||||
| 	"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather" | ||||
| ) | ||||
|  | ||||
| func (r *RedisRecorder) Set(ctx context.Context, u *weather.WeatherUpdate) error { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.set", trace.WithAttributes( | ||||
| 		attribute.String("stationName", u.GetStationName()), | ||||
| 		attribute.String("stationType", util.DerefStr(u.StationType)), | ||||
| 	)) | ||||
| 	defer span.End() | ||||
|  | ||||
| 	r.Lock() | ||||
| 	defer r.RUnlock() | ||||
|  | ||||
| 	// First ensure we can prepare our payload | ||||
| 	data, err := json.Marshal(u) | ||||
| 	if err != nil { | ||||
| 		span.RecordError(err) | ||||
| 		span.SetStatus(codes.Error, err.Error()) | ||||
|  | ||||
| 		r.log.Err(err).Send() | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return r.set(ctx, data) | ||||
| } | ||||
|  | ||||
| func (r *RedisRecorder) set(ctx context.Context, data []byte) error { | ||||
| 	ctx, span := r.tracer.Start(ctx, "redisRecorder.set.push", trace.WithAttributes( | ||||
| 		attribute.Int("updateBytes", len(data)), | ||||
| 	)) | ||||
| 	defer span.End() | ||||
|  | ||||
| 	// Atomic, push and trim | ||||
| 	tx := r.redis.TxPipeline() | ||||
| 	tx.LPush(ctx, r.Key(), data) | ||||
| 	tx.LTrim(ctx, r.Key(), 0, int64(r.keep)-1) | ||||
|  | ||||
| 	if rErr, err := tx.Exec(ctx); err != nil { | ||||
| 		for _, cmd := range rErr { | ||||
| 			span.RecordError(cmd.Err()) | ||||
| 		} | ||||
| 		span.SetStatus(codes.Error, err.Error()) | ||||
| 		r.log.Err(err).Send() | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	span.SetAttributes(attribute.Int("updateCount", r.count(ctx))) | ||||
| 	span.SetStatus(codes.Ok, "") | ||||
| 	return nil | ||||
| } | ||||
		Reference in New Issue
	
	Block a user