5
5
"encoding/json"
6
6
"fmt"
7
7
"strconv"
8
- "sync"
9
8
"time"
10
9
11
10
"github.com/harness/ff-golang-server-sdk/sdk_codes"
@@ -32,8 +31,20 @@ const (
32
31
sdkLanguageAttribute string = "SDK_LANGUAGE"
33
32
sdkLanguage string = "go"
34
33
globalTarget string = "global"
34
+ maxAnalyticsEntries int = 10000
35
+ maxTargetEntries int = 100000
35
36
)
36
37
38
+ // SafeAnalyticsCache is a type that provides thread safe access to maps used by analytics
39
+ type SafeAnalyticsCache [K comparable , V any ] interface {
40
+ set (key K , value V )
41
+ get (key K ) (V , bool )
42
+ delete (key K )
43
+ size () int
44
+ clear ()
45
+ iterate (func (K , V ))
46
+ }
47
+
37
48
type analyticsEvent struct {
38
49
target * evaluation.Target
39
50
featureConfig * rest.FeatureConfig
@@ -43,13 +54,14 @@ type analyticsEvent struct {
43
54
44
55
// AnalyticsService provides a way to cache and send analytics to the server
45
56
type AnalyticsService struct {
46
- mx * sync.Mutex
47
- analyticsChan chan analyticsEvent
48
- analyticsData map [string ]analyticsEvent
49
- timeout time.Duration
50
- logger logger.Logger
51
- metricsClient * metricsclient.ClientWithResponsesInterface
52
- environmentID string
57
+ analyticsChan chan analyticsEvent
58
+ evaluationAnalytics SafeAnalyticsCache [string , analyticsEvent ]
59
+ targetAnalytics SafeAnalyticsCache [string , evaluation.Target ]
60
+ seenTargets SafeAnalyticsCache [string , bool ]
61
+ timeout time.Duration
62
+ logger logger.Logger
63
+ metricsClient metricsclient.ClientWithResponsesInterface
64
+ environmentID string
53
65
}
54
66
55
67
// NewAnalyticsService creates and starts a analytics service to send data to the client
@@ -61,19 +73,20 @@ func NewAnalyticsService(timeout time.Duration, logger logger.Logger) *Analytics
61
73
serviceTimeout = 1 * time .Hour
62
74
}
63
75
as := AnalyticsService {
64
- mx : & sync.Mutex {},
65
- analyticsChan : make (chan analyticsEvent ),
66
- analyticsData : map [string ]analyticsEvent {},
67
- timeout : serviceTimeout ,
68
- logger : logger ,
76
+ analyticsChan : make (chan analyticsEvent ),
77
+ evaluationAnalytics : newSafeEvaluationAnalytics (),
78
+ targetAnalytics : newSafeTargetAnalytics (),
79
+ seenTargets : newSafeSeenTargets (),
80
+ timeout : serviceTimeout ,
81
+ logger : logger ,
69
82
}
70
83
go as .listener ()
71
84
72
85
return & as
73
86
}
74
87
75
88
// Start starts the client and timer to send analytics
76
- func (as * AnalyticsService ) Start (ctx context.Context , client * metricsclient.ClientWithResponsesInterface , environmentID string ) {
89
+ func (as * AnalyticsService ) Start (ctx context.Context , client metricsclient.ClientWithResponsesInterface , environmentID string ) {
77
90
as .logger .Infof ("%s Metrics started" , sdk_codes .MetricsStarted )
78
91
as .metricsClient = client
79
92
as .environmentID = environmentID
@@ -106,18 +119,44 @@ func (as *AnalyticsService) PushToQueue(featureConfig *rest.FeatureConfig, targe
106
119
func (as * AnalyticsService ) listener () {
107
120
as .logger .Info ("Analytics cache successfully initialized" )
108
121
for ad := range as .analyticsChan {
109
- key := getEventSummaryKey (ad )
122
+ analyticsKey := getEvaluationAnalyticKey (ad )
123
+
124
+ // Check if we've hit capacity for evaluations
125
+ if as .evaluationAnalytics .size () < maxAnalyticsEntries {
126
+ // Update evaluation metrics
127
+ analytic , ok := as .evaluationAnalytics .get (analyticsKey )
128
+ if ! ok {
129
+ ad .count = 1
130
+ as .evaluationAnalytics .set (analyticsKey , ad )
131
+ } else {
132
+ ad .count = analytic .count + 1
133
+ as .evaluationAnalytics .set (analyticsKey , ad )
134
+ }
135
+ } else {
136
+ as .logger .Warnf ("%s Evaluation analytic cache reached max size, remaining evaluation metrics for this analytics interval will not be sent" , sdk_codes .EvaluationMetricsMaxSizeReached )
137
+ }
138
+
139
+ // Check if target is nil or anonymous
140
+ if ad .target == nil || (ad .target .Anonymous != nil && * ad .target .Anonymous ) {
141
+ continue
142
+ }
110
143
111
- as .mx .Lock ()
112
- analytic , ok := as .analyticsData [key ]
113
- if ! ok {
114
- ad .count = 1
115
- as .analyticsData [key ] = ad
144
+ // Check if target has been seen
145
+ _ , seen := as .seenTargets .get (ad .target .Identifier )
146
+
147
+ if seen {
148
+ continue
149
+ }
150
+
151
+ // Update seen targets
152
+ as .seenTargets .set (ad .target .Identifier , true )
153
+
154
+ // Update target metrics
155
+ if as .targetAnalytics .size () < maxTargetEntries {
156
+ as .targetAnalytics .set (ad .target .Identifier , * ad .target )
116
157
} else {
117
- ad .count = (analytic .count + 1 )
118
- as .analyticsData [key ] = ad
158
+ as .logger .Warnf ("%s Target analytics cache reached max size, remaining target metrics for this analytics interval will not be sent" , sdk_codes .TargetMetricsMaxSizeReached )
119
159
}
120
- as .mx .Unlock ()
121
160
}
122
161
}
123
162
@@ -150,116 +189,75 @@ func convertInterfaceToString(i interface{}) string {
150
189
}
151
190
152
191
func (as * AnalyticsService ) sendDataAndResetCache (ctx context.Context ) {
153
- as .mx .Lock ()
154
- // copy cache to send to server
155
- analyticsData := as .analyticsData
156
- // clear cache. As metrics is secondary to the flags, we do it this way
157
- // so it doesn't effect the performance of our users code. Even if it means
158
- // we lose metrics the odd time.
159
- as .analyticsData = map [string ]analyticsEvent {}
160
- as .mx .Unlock ()
161
-
162
- metricData := make ([]metricsclient.MetricsData , 0 , len (as .analyticsData ))
163
- targetData := map [string ]metricsclient.TargetData {}
164
-
165
- for _ , analytic := range analyticsData {
166
- if analytic .target != nil {
167
- if analytic .target .Anonymous == nil || ! * analytic .target .Anonymous {
168
- targetAttributes := make ([]metricsclient.KeyValue , 0 )
169
- if analytic .target .Attributes != nil {
170
- targetAttributes = make ([]metricsclient.KeyValue , 0 , len (* analytic .target .Attributes ))
171
- for key , value := range * analytic .target .Attributes {
172
- v := convertInterfaceToString (value )
173
- kv := metricsclient.KeyValue {
174
- Key : key ,
175
- Value : v ,
176
- }
177
- targetAttributes = append (targetAttributes , kv )
178
- }
179
-
180
- }
181
-
182
- targetName := analytic .target .Identifier
183
- if analytic .target .Name != "" {
184
- targetName = analytic .target .Name
185
- }
186
-
187
- td := metricsclient.TargetData {
188
- Name : targetName ,
189
- Identifier : analytic .target .Identifier ,
190
- Attributes : targetAttributes ,
191
- }
192
- targetData [analytic .target .Identifier ] = td
193
- }
194
- }
195
192
193
+ // Clone and reset the evaluation analytics cache to minimise the duration
194
+ // for which locks are held, so that metrics processing does not affect flag evaluations performance.
195
+ // Although this might occasionally result in the loss of some metrics during periods of high load,
196
+ // it is an acceptable tradeoff to prevent extended lock periods that could degrade user code.
197
+ evaluationAnalyticsClone := as .evaluationAnalytics
198
+
199
+ as .evaluationAnalytics = newSafeEvaluationAnalytics ()
200
+
201
+ // Clone and reset target analytics cache for same reason.
202
+ targetAnalyticsClone := as .targetAnalytics
203
+ as .targetAnalytics = newSafeTargetAnalytics ()
204
+
205
+ metricData := make ([]metricsclient.MetricsData , 0 , evaluationAnalyticsClone .size ())
206
+ targetData := make ([]metricsclient.TargetData , 0 , targetAnalyticsClone .size ())
207
+
208
+ // Process evaluation metrics
209
+ evaluationAnalyticsClone .iterate (func (key string , analytic analyticsEvent ) {
196
210
metricAttributes := []metricsclient.KeyValue {
197
- {
198
- Key : featureIdentifierAttribute ,
199
- Value : analytic .featureConfig .Feature ,
200
- },
201
- {
202
- Key : featureNameAttribute ,
203
- Value : analytic .featureConfig .Feature ,
204
- },
205
- {
206
- Key : variationIdentifierAttribute ,
207
- Value : analytic .variation .Identifier ,
208
- },
209
- {
210
- Key : variationValueAttribute ,
211
- Value : analytic .variation .Value ,
212
- },
213
- {
214
- Key : sdkTypeAttribute ,
215
- Value : sdkType ,
216
- },
217
- {
218
- Key : sdkLanguageAttribute ,
219
- Value : sdkLanguage ,
220
- },
221
- {
222
- Key : sdkVersionAttribute ,
223
- Value : SdkVersion ,
224
- },
211
+ {Key : featureIdentifierAttribute , Value : analytic .featureConfig .Feature },
212
+ {Key : featureNameAttribute , Value : analytic .featureConfig .Feature },
213
+ {Key : variationIdentifierAttribute , Value : analytic .variation .Identifier },
214
+ {Key : variationValueAttribute , Value : analytic .variation .Value },
215
+ {Key : sdkTypeAttribute , Value : sdkType },
216
+ {Key : sdkLanguageAttribute , Value : sdkLanguage },
217
+ {Key : sdkVersionAttribute , Value : SdkVersion },
218
+ {Key : targetAttribute , Value : globalTarget },
225
219
}
226
220
227
- metricAttributes = append (metricAttributes , metricsclient.KeyValue {
228
- Key : targetAttribute ,
229
- Value : globalTarget ,
230
- })
231
-
232
221
md := metricsclient.MetricsData {
233
222
Timestamp : time .Now ().UnixNano () / (int64 (time .Millisecond ) / int64 (time .Nanosecond )),
234
223
Count : analytic .count ,
235
224
MetricsType : metricsclient .MetricsDataMetricsType (ffMetricType ),
236
225
Attributes : metricAttributes ,
237
226
}
238
227
metricData = append (metricData , md )
239
- }
228
+ })
240
229
241
- // if targets data is empty we just send nil
242
- var targetDataPayload * []metricsclient.TargetData = nil
243
- if len (targetData ) > 0 {
244
- targetDataPayload = targetDataMapToArray (targetData )
245
- }
230
+ // Process target metrics
231
+ targetAnalyticsClone .iterate (func (key string , target evaluation.Target ) {
232
+ targetAttributes := make ([]metricsclient.KeyValue , 0 )
233
+ for key , value := range * target .Attributes {
234
+ targetAttributes = append (targetAttributes , metricsclient.KeyValue {Key : key , Value : convertInterfaceToString (value )})
235
+ }
236
+
237
+ td := metricsclient.TargetData {
238
+ Identifier : target .Identifier ,
239
+ Name : target .Name ,
240
+ Attributes : targetAttributes ,
241
+ }
242
+ targetData = append (targetData , td )
243
+ })
246
244
247
245
analyticsPayload := metricsclient.PostMetricsJSONRequestBody {
248
246
MetricsData : & metricData ,
249
- TargetData : targetDataPayload ,
247
+ TargetData : & targetData ,
250
248
}
251
249
252
250
if as .metricsClient != nil {
253
- emptyMetricsData := analyticsPayload . MetricsData == nil || len (* analyticsPayload . MetricsData ) == 0
254
- emptyTargetData := analyticsPayload . TargetData == nil || len (* analyticsPayload . TargetData ) == 0
251
+ emptyMetricsData := len (metricData ) == 0
252
+ emptyTargetData := len (targetData ) == 0
255
253
256
254
// if we have no metrics to send skip the post request
257
255
if emptyMetricsData && emptyTargetData {
258
256
as .logger .Debug ("No metrics or target data to send" )
259
257
return
260
258
}
261
259
262
- mClient := * as .metricsClient
260
+ mClient := as .metricsClient
263
261
264
262
jsonData , err := json .Marshal (analyticsPayload )
265
263
if err != nil {
@@ -287,22 +285,6 @@ func (as *AnalyticsService) sendDataAndResetCache(ctx context.Context) {
287
285
}
288
286
}
289
287
290
- //func getEventKey(event analyticsEvent) string {
291
- // targetIdentifier := ""
292
- // if event.target != nil {
293
- // targetIdentifier = event.target.Identifier
294
- // }
295
- // return fmt.Sprintf("%s-%s-%s-%s", event.featureConfig.Feature, event.variation.Identifier, event.variation.Value, targetIdentifier)
296
- //}
297
-
298
- func getEventSummaryKey (event analyticsEvent ) string {
288
+ func getEvaluationAnalyticKey (event analyticsEvent ) string {
299
289
return fmt .Sprintf ("%s-%s-%s-%s" , event .featureConfig .Feature , event .variation .Identifier , event .variation .Value , globalTarget )
300
290
}
301
-
302
- func targetDataMapToArray (targetMap map [string ]metricsclient.TargetData ) * []metricsclient.TargetData {
303
- targetDataArray := make ([]metricsclient.TargetData , 0 , len (targetMap ))
304
- for _ , targetData := range targetMap {
305
- targetDataArray = append (targetDataArray , targetData )
306
- }
307
- return & targetDataArray
308
- }
0 commit comments