По какому ключу определять методы для счетчика запросов
Что проверяют
Понимаете ли вы, что ключ счётчика это компромисс между детальностью и кардинальностью, и знаете ли, чем инкремент в многопоточном приложении отличается от инкремента в учебнике. Вопрос про метрики, а не про коллекцию.
Как отвечать
Ключом берите то, у чего ограниченное число значений. Для веб-слоя это шаблон маршрута, метод и статус, а не конкретный адрес:
Counter.builder("api.requests")
.tag("uri", "/api/orders/{id}") // шаблон, а не /api/orders/123
.tag("method", "POST")
.tag("status", "500")
.register(registry);Для внутренних вызовов ключ это класс и метод, полученные из точки среза, либо имя логической операции. Идентификаторы, параметры запроса, адрес клиента и почту в ключ не кладут: каждое новое значение это новый временной ряд.
Вторая половина ответа про реализацию. Счётчик конкурентный по определению, поэтому нужна конкурентная коллекция и накопитель без блокировки:
counts.computeIfAbsent(key, k -> new LongAdder()).increment();LongAdder разносит инкременты по ячейкам и на высокой конкуренции обгоняет AtomicLong. В обычном приложении писать это руками не нужно: Micrometer вместе с Actuator уже считает запросы по шаблону маршрута, а свои теги добавляются туда же.
Где спотыкаются
Кладут в ключ полный адрес с идентификатором или параметрами. Через сутки в памяти сотни тысяч ключей, хранилище метрик задыхается, а построить по такому набору всё равно ничего нельзя: каждая строка встречается однажды.
Второе: берут обычный HashMap и пару из чтения и записи. Инкременты теряются на гонке, а параллельное расширение таблицы способно испортить саму структуру, и воспроизвести это под отладчиком почти невозможно.
Третье: забывают про границу набора ключей. Счётчик по значению, которое приходит снаружи, это готовый вектор отказа: достаточно слать случайные пути, чтобы заполнить память. Ключ должен браться из конечного набора, известного приложению заранее.