8000 Add UsageMultiSetCounter · localstack/localstack@c926c7c · GitHub
[go: up one dir, main page]

Skip to content

Commit c926c7c

Browse files
committed
Add UsageMultiSetCounter
1 parent 5f19fbc commit c926c7c

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

localstack-core/localstack/utils/analytics/usage.py

Lines changed: 58 additions & 0 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import math
3+
import threading
34
from collections import defaultdict
45
from itertools import count
56
from typing import Any
@@ -49,6 +50,63 @@ def aggregate(self) -> dict:
4950
return self.state
5051

5152

53+
class UsageMultiSetCounter:
54+
"""
55+
Use this counter to count occurrences of unique values for multiple dimensions.
56+
This dynamically creates UsageSetCounters and should be used with care (i.e., with limited keys).
57+
Example:
58+
my_feature_counter = UsageMultiSetCounter("pipes:invocation")
59+
my_feature_counter.record("aws:sqs", "aws:lambda")
60+
my_feature_counter.record("aws:sqs", "aws:lambda")
61+
my_feature_counter.record("aws:sqs", "aws:stepfunctions")
62+
my_feature_counter.record("aws:kinesis", "aws:lambda")
63+
aggregate is implemented for each counter individually
64+
my_feature_counter.aggregate() is available for testing purposes:
65+
{
66+
"aws:sqs": {
67+
"aws:lambda": 2,
68+
"aws:stepfunctions": 1,
69+
},
70+
"aws:kinesis": {
71+
"aws:lambda": 1
72+
}
73+
}
74+
"""
75+
76+
namespace: str
77+
_counters: dict[str, UsageSetCounter]
78+
lock = threading.Lock()
79+
80+
def __init__(self, namespace: str):
81+
self._counters = {}
82+
self.namespace = namespace
83+
84+
def record(self, key: str, value: str):
85+
namespace = f"{self.namespace}:{key}"
86+
if namespace in self._counters:
87+
set_counter = self._counters[namespace]
88+
else:
89+
with self.lock:
90+
if namespace in self._counters:
91+
set_counter = self._counters[namespace]
92+
else:
93+
# We cannot use setdefault here because Python always instantiates a new UsageSetCounter,
94+
# which overwrites the collector_registry
95+
set_counter = UsageSetCounter(namespace)
96+
self._counters[namespace] = set_counter
97+
98+
self._counters[namespace] = set_counter
99+
set_counter.record(value)
100+
101+
def aggregate(self) -> dict:
102+
"""aggregate is invoked on a per UsageSetCounter level because each counter is registered individually.
103+
This utility is only for testing!"""
104+
merged_dict = {}
105+
for namespace, counter in self._counters.items():
106+
merged_dict[namespace] = counter.aggregate()
107+
return merged_dict
108+
109+
52110
class UsageCounter:
53111
"""
54112
Use this counter to count numeric values
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from localstack.utils.analytics.usage import UsageMultiSetCounter
2+
3+
4+
def test_multi_set_counter():
5+
my_feature_counter = UsageMultiSetCounter("pipes:invocation")
6+
my_feature_counter.record("aws:sqs", "aws:lambda")
7+
my_feature_counter.record("aws:sqs", "aws:lambda")
8+
my_feature_counter.record("aws:sqs", "aws:stepfunctions")
9+
my_feature_counter.record("aws:kinesis", "aws:lambda")
10+
assert my_feature_counter.aggregate() == {
11+
"pipes:invocation:aws:sqs": {
12+
"aws:lambda": 2,
13+
"aws:stepfunctions": 1,
14+
},
15+
"pipes:invocation:aws:kinesis": {"aws:lambda": 1},
16+
}
17+
assert my_feature_counter._counters["pipes:invocation:aws:sqs"].state == {
18+
"aws:lambda": 2,
19+
"aws:stepfunctions": 1,
20+
}
21+
assert my_feature_counter._counters["pipes:invocation:aws:kinesis"].state == {"aws:lambda": 1}

0 commit comments

Comments
 (0)
0