@@ -87,7 +87,8 @@ public void recordAggregateData(final PeriodicData periodicData) {
87
87
periodicData .getDimensions (),
88
88
_periodDimensionName ,
89
89
periodicData .getPeriod (),
90
- _mappedDimensions );
90
+ _mappedDimensions ,
91
+ _defaultDimensionValues );
91
92
_buckets .computeIfAbsent (bucketKey , k -> new Bucket (bucketKey ))
92
93
.record (periodicData , now );
93
94
}
@@ -116,7 +117,8 @@ static Key computeKey(
116
117
final Key key ,
117
118
final String periodDimensionName ,
118
119
final Duration period ,
119
- final ImmutableMultimap <String , String > mappedDimensions ) {
120
+ final ImmutableMultimap <String , String > mappedDimensions ,
121
+ final ImmutableMap <String , String > defaultDimensionValues ) {
120
122
// TODO(ville): Apply interning to creating Key instances
121
123
122
124
// Filter the input key's parameters by the ones we wish to dimension map
@@ -134,10 +136,10 @@ static Key computeKey(
134
136
135
137
mappedDimensions .entries ()
136
138
.stream ()
137
- .filter (e -> parameters .containsKey (e .getKey ()))
139
+ .filter (e -> parameters .containsKey (e .getKey ()) || defaultDimensionValues . containsKey ( e . getKey ()) )
138
140
.forEach (e -> dimensionsBuilder .put (
139
141
e .getValue (),
140
- parameters .get (e .getKey ())));
142
+ parameters .getOrDefault ( e . getKey (), defaultDimensionValues . get (e .getKey () ))));
141
143
142
144
return new DefaultKey (dimensionsBuilder .build ());
143
145
}
@@ -147,7 +149,8 @@ private void flushMetrics() {
147
149
// condition between the flush+remove and any new data added.
148
150
//
149
151
// 1) If the bucket flush does nothing then remove it from the buckets map and
150
- // add it to a "to be removed" list. This prevents further
152
+ // add it to a "to be removed" list. This prevents buckets from accumulating
153
+ // in memory if the tag space changes over time.
151
154
//
152
155
// 2) On the next flush interval re-flush all the buckets to be removed and then
153
156
// actually drop them.
@@ -206,13 +209,14 @@ private static String getPeriodAsString(final Duration period) {
206
209
//
207
210
// The validation in the Builder ensures that all target key names
208
211
// occur only once across dimensions and mappedDimensions regardless
209
- // of what the source key name is (or whether it was unammped or mapped).
212
+ // of what the source key name is (or whether it was unmapped or mapped).
210
213
final ImmutableMultimap .Builder <String , String > dimensionsBuilder = ImmutableMultimap .builder ();
211
214
builder ._dimensions .forEach (e -> dimensionsBuilder .put (e , e ));
212
215
dimensionsBuilder .putAll (builder ._mappedDimensions .entrySet ());
213
216
214
217
// Initialize the metrics factory and metrics instance
215
218
_mappedDimensions = dimensionsBuilder .build ();
219
+ _defaultDimensionValues = builder ._defaultDimensionValues ;
216
220
_metricsFactory = builder ._metricsFactory ;
217
221
_buckets = Maps .newConcurrentMap ();
218
222
_periodDimensionName = builder ._periodDimensionName ;
@@ -238,6 +242,7 @@ private PeriodicStatisticsSink(final Builder builder) {
238
242
}
239
243
240
244
private final ImmutableMultimap <String , String > _mappedDimensions ;
245
+ private final ImmutableMap <String , String > _defaultDimensionValues ;
241
246
private final Map <Key , Bucket > _buckets ;
242
247
private final MetricsFactory _metricsFactory ;
243
248
private final Deque <Bucket > _bucketsToBeRemoved = new ConcurrentLinkedDeque <>();
@@ -329,33 +334,39 @@ public void record(final PeriodicData periodicData, final long now) {
329
334
330
335
// This assumes that all AggregatedData instances have the
331
336
// population field set correctly (really they should).
332
- _metricSamples .accumulate (firstDatum .getPopulationSize ());
337
+ _metricSamples .addAndGet (firstDatum .getPopulationSize ());
333
338
}
334
339
335
340
_age .accumulate (now - periodicData .getStart ().plus (periodicData .getPeriod ()).toInstant ().toEpochMilli ());
336
341
}
337
342
338
343
public boolean flushMetrics () {
339
- // Rotate the metrics instance
340
- final Metrics metrics = _metrics .getAndSet (createMetrics ());
341
-
342
344
// Gather and reset state
343
345
final Set <String > oldUniqueMetrics = _uniqueMetrics .getAndSet (
344
346
createConcurrentSet (_uniqueMetrics .get ()));
345
347
final Set <MetricKey > oldUniqueStatistics = _uniqueStatistics .getAndSet (
346
348
createConcurrentSet (_uniqueStatistics .get ()));
347
349
348
- // Record statistics and close
349
- metrics .incrementCounter (_aggregatedDataName , _aggregatedData .getAndSet (0 ));
350
- metrics .incrementCounter (_uniqueMetricsName , oldUniqueMetrics .size ());
351
- metrics .incrementCounter (_uniqueStatisticsName , oldUniqueStatistics .size ());
352
- metrics .incrementCounter (_metricSamplesName , _metricSamples .getThenReset ());
353
- metrics .setTimer (_ageName , _age .getThenReset (), TimeUnit .MILLISECONDS );
354
- metrics .close ();
355
-
356
350
// Use unique metrics as a proxy for whether data was added. See note in the sink's
357
351
// flushMetrics method about how the race condition is resolved.
358
- return !oldUniqueMetrics .isEmpty ();
352
+ if (!oldUniqueMetrics .isEmpty ()) {
353
+ // Rotate the metrics instance
354
+ final Metrics metrics = _metrics .getAndSet (createMetrics ());
355
+
356
+ // Record statistics and close
357
+ metrics .incrementCounter (_aggregatedDataName , _aggregatedData .getAndSet (0 ));
358
+ metrics .incrementCounter (_uniqueMetricsName , oldUniqueMetrics .size ());
359
+ metrics .incrementCounter (_uniqueStatisticsName , oldUniqueStatistics .size ());
360
+ metrics .incrementCounter (_metricSamplesName , _metricSamples .getAndSet (0 ));
361
+ metrics .setTimer (_ageName , _age .getThenReset (), TimeUnit .MILLISECONDS );
362
+ metrics .close ();
363
+
364
+ // Periodic data was flushed
365
+ return true ;
366
+ }
367
+
368
+ // No periodic data was flushed; this bucket can be cleaned up
369
+ return false ;
359
370
}
360
371
361
372
@ LogValue
@@ -371,10 +382,6 @@ public Object toLogValue() {
371
382
private Metrics createMetrics () {
372
383
final Metrics metrics = _metricsFactory .create ();
373
384
metrics .addAnnotations (_key .getParameters ());
374
- metrics .resetCounter (_aggregatedDataName );
375
- metrics .resetCounter (_uniqueMetricsName );
376
- metrics .resetCounter (_uniqueStatisticsName );
377
- metrics .resetCounter (_metricSamplesName );
378
385
return metrics ;
379
386
}
380
387
@@ -387,7 +394,7 @@ private Metrics createMetrics() {
387
394
private final AtomicReference <Metrics > _metrics = new AtomicReference <>();
388
395
389
396
private final LongAccumulator _age = new LongAccumulator (Math ::max , 0 );
390
- private final LongAccumulator _metricSamples = new LongAccumulator ( Long :: sum , 0 );
397
+ private final AtomicLong _metricSamples = new AtomicLong ( 0 );
391
398
private final AtomicLong _aggregatedData = new AtomicLong (0 );
392
399
private final AtomicReference <Set <String >> _uniqueMetrics = new AtomicReference <>(
393
400
ConcurrentHashMap .newKeySet ());
@@ -468,6 +475,20 @@ public Builder setMappedDimensions(final ImmutableMap<String, String> value) {
468
475
return this ;
469
476
}
470
477
478
+ /**
479
+ * Supply default dimension values by original dimension key. The keys
480
+ * in this map must also be specified either in {@link Builder#setDimensions(ImmutableSet)}
481
+ * or as keys in {@link Builder#setMappedDimensions(ImmutableMap)}. The
482
+ * key refers to the dimension name on the input {@link PeriodicData}.
483
+ *
484
+ * @param value The default dimension key-value pairs.
485
+ * @return This instance of <code>Builder</code>.
486
+ */
487
+ public Builder setDefaultDimensionsValues (final ImmutableMap <String , String > value ) {
488
+ _defaultDimensionValues = value ;
489
+ return this ;
490
+ }
491
+
471
492
/**
472
493
* The name of the outbound dimension for the periodicity of the data.
473
494
* Cannot be null. Default is "_period".
@@ -506,6 +527,9 @@ protected Builder self() {
506
527
@ NotNull
507
528
private ImmutableMap <String , String > _mappedDimensions = ImmutableMap .of ();
508
529
@ NotNull
530
+ @ CheckWith (value = CheckDefaultDimensionTargets .class )
531
+ private ImmutableMap <String , String > _defaultDimensionValues = ImmutableMap .of ();
532
+ @ NotNull
509
533
private String _periodDimensionName = "_period" ;
510
534
@ JacksonInject
511
535
@ NotNull
@@ -552,7 +576,7 @@ public boolean isSatisfied(final Object validatedObject, final Object value) {
552
576
LOGGER .warn ()
553
577
.setMessage ("Invalid PeriodicStatisticsSink" )
554
578
.addData ("reason" , "Mapped dimensions overlap with periodDimensionName" )
555
- .addData ("dimensions " , builder ._periodDimensionName )
579
+ .addData ("periodDimensionName " , builder ._periodDimensionName )
556
580
.addData ("mappedDimensions" , builder ._mappedDimensions )
557
581
.log ();
558
582
return false ;
@@ -561,7 +585,7 @@ public boolean isSatisfied(final Object validatedObject, final Object value) {
561
585
LOGGER .warn ()
562
586
.setMessage ("Invalid PeriodicStatisticsSink" )
563
587
.addData ("reason" , "(Unmapped) dimensions overlap with periodDimensionName" )
564
- .addData ("dimensions " , builder ._periodDimensionName )
588
+ .addData ("periodDimensionName " , builder ._periodDimensionName )
565
589
.addData ("dimensions" , builder ._dimensions )
566
590
.log ();
567
591
return false ;
@@ -577,8 +601,40 @@ public boolean isSatisfied(final Object validatedObject, final Object value) {
577
601
// mapped dimension rule for "b" are equivalent! However, the current
578
602
// check disallows this.
579
603
//
604
+ // See test:
605
+ // testValidationDimensionCollisionAlthoughLogicallyEquivalent
606
+ //
580
607
// TODO(ville): Once we have a use case address this.
581
608
609
+ return true ;
610
+ }
611
+ }
612
+
613
+ private static final class CheckDefaultDimensionTargets implements CheckWithCheck .SimpleCheck {
614
+
615
+ private static final long serialVersionUID = 5011108547193627318L ;
616
+
617
+ @ Override
618
+ public boolean isSatisfied (final Object validatedObject , final Object value ) {
619
+ // TODO(ville): Find a way to throw validation exceptions instead of logging.
620
+
621
+ if (!(validatedObject instanceof PeriodicStatisticsSink .Builder )) {
622
+ return false ;
623
+ }
624
+ final PeriodicStatisticsSink .Builder builder = (PeriodicStatisticsSink .Builder ) validatedObject ;
625
+
626
+ for (final String keyToDefault : builder ._defaultDimensionValues .keySet ()) {
627
+ if (!builder ._dimensions .contains (keyToDefault ) && !builder ._mappedDimensions .containsKey (keyToDefault )) {
628
+ LOGGER .warn ()
629
+ .setMessage ("Invalid PeriodicStatisticsSink" )
630
+ .addData ("reason" , "Default dimensions key not specified in (unmapped) dimensions or mapped dimensions" )
631
+ .addData ("dimensions" , builder ._dimensions )
632
+ .addData ("mappedDimensions.keySet" , builder ._mappedDimensions .keySet ())
633
+ .addData ("defaultDimensionKey" , keyToDefault )
634
+ .log ();
635
+ return false ;
636
+ }
637
+ }
582
638
583
639
return true ;
584
640
}
0 commit comments