Overview
Custom metric tags provide dimensional data to metrics, enabling powerful filtering, grouping, and analysis capabilities. This guide covers comprehensive implementations across major Java monitoring frameworks.
Core Concepts
1. Tag Types
- Static Tags: Fixed key-value pairs applied to all metrics
- Dynamic Tags: Context-dependent tags added at runtime
- Hierarchical Tags: Nested or namespaced tag structures
- Cardinality-aware Tags: Low-cardinality tags for performance
2. Tag Best Practices
- Low Cardinality: Avoid high-variance values
- Consistent Naming: Use standardized tag names
- Performance Awareness: Minimize tag creation overhead
- Semantic Meaning: Ensure tags provide useful dimensions
Micrometer Implementation
1. Basic Tag Configuration
@Configuration public class MicrometerTagConfig { @Bean public MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); } @Bean public MeterFilter commonTagsMeterFilter() { return MeterFilter.commonTags(Arrays.asList( Tag.of("application", "order-service"), Tag.of("environment", System.getenv("APP_ENV")), Tag.of("version", "1.0.0"), Tag.of("cluster", System.getenv("K8S_CLUSTER")) )); } @Bean public MeterFilter cardinalityLimitFilter() { return new MeterFilter() { @Override public Meter.Id map(Meter.Id id) { // Limit tag cardinality if (id.getName().startsWith("http.requests")) { return id.withTag(Tag.of("status", id.getTag("status") != null ? id.getTag("status") : "unknown")); } return id; } }; } } 2. Dynamic Tag Management
@Component public class DynamicTagManager { private final MeterRegistry meterRegistry; private final ThreadLocal<Map<String, String>> contextTags = ThreadLocal.withInitial(HashMap::new); public DynamicTagManager(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } public void setContextTag(String key, String value) { contextTags.get().put(key, value); } public void clearContextTag(String key) { contextTags.get().remove(key); } public void clearAllContextTags() { contextTags.get().clear(); } public Timer timerWithContext(String name, String... additionalTags) { List<Tag> allTags = buildTagsWithContext(additionalTags); return Timer.builder(name) .tags(allTags) .register(meterRegistry); } public Counter counterWithContext(String name, String... additionalTags) { List<Tag> allTags = buildTagsWithContext(additionalTags); return Counter.builder(name) .tags(allTags) .register(meterRegistry); } private List<Tag> buildTagsWithContext(String... additionalTags) { Map<String, String> currentTags = contextTags.get(); List<Tag> tags = new ArrayList<>(); // Add context tags currentTags.forEach((key, value) -> { if (value != null) { tags.add(Tag.of(key, value)); } }); // Add additional tags for (int i = 0; i < additionalTags.length; i += 2) { if (i + 1 < additionalTags.length) { tags.add(Tag.of(additionalTags[i], additionalTags[i + 1])); } } return tags; } } 3. Aspect-Oware Tagging
@Aspect @Component public class MetricTagAspect { private final DynamicTagManager tagManager; private final MeterRegistry meterRegistry; public MetricTagAspect(DynamicTagManager tagManager, MeterRegistry meterRegistry) { this.tagManager = tagManager; this.meterRegistry = meterRegistry; } @Around("@annotation(metricTags)") public Object applyMetricTags(ProceedingJoinPoint joinPoint, MetricTags metricTags) throws Throwable { try { // Extract tags from annotation and method parameters Map<String, String> extractedTags = extractTags(joinPoint, metricTags); // Set context tags extractedTags.forEach(tagManager::setContextTag); // Execute method with tags return joinPoint.proceed(); } finally { // Clear context tags tagManager.clearAllContextTags(); } } @Around("execution(* com.example.service.*.*(..))") public Object measureServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable { String className = joinPoint.getTarget().getClass().getSimpleName(); String methodName = joinPoint.getSignature().getName(); Timer.Sample sample = Timer.start(meterRegistry); try { Object result = joinPoint.proceed(); String status = "success"; return result; } catch (Exception e) { String status = "error"; String errorType = e.getClass().getSimpleName(); tagManager.setContextTag("error_type", errorType); throw e; } finally { sample.stop(Timer.builder("service.method.duration") .tag("class", className) .tag("method", methodName) .tag("status", tagManager.getContextTag("status")) .tag("error_type", tagManager.getContextTag("error_type")) .register(meterRegistry)); tagManager.clearAllContextTags(); } } private Map<String, String> extractTags(ProceedingJoinPoint joinPoint, MetricTags annotation) { Map<String, String> tags = new HashMap<>(); // Extract from annotation values for (TagValue tagValue : annotation.value()) { String value = evaluateSpEL(tagValue.expression(), joinPoint); tags.put(tagValue.key(), value); } return tags; } private String evaluateSpEL(String expression, ProceedingJoinPoint joinPoint) { // Implement SpEL evaluation for dynamic tag values return expression; // Simplified } } // Custom annotation for metric tagging @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MetricTags { TagValue[] value() default {}; } @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface TagValue { String key(); String expression(); // SpEL expression } Dropwizard Metrics Implementation
1. Tagged Metric Registry
public class TaggedMetricRegistry { private final MetricRegistry metricRegistry; private final Map<MetricKey, Metric> taggedMetrics; private final TagResolver tagResolver; public TaggedMetricRegistry(MetricRegistry metricRegistry, TagResolver tagResolver) { this.metricRegistry = metricRegistry; this.taggedMetrics = new ConcurrentHashMap<>(); this.tagResolver = tagResolver; } public Counter counter(String name, Tag... tags) { MetricKey key = new MetricKey(name, tags); return taggedMetrics.computeIfAbsent(key, k -> { String fullName = buildMetricName(name, tags); return metricRegistry.counter(fullName); }); } public Timer timer(String name, Tag... tags) { MetricKey key = new MetricKey(name, tags); return taggedMetrics.computeIfAbsent(key, k -> { String fullName = buildMetricName(name, tags); return metricRegistry.timer(fullName); }); } public Histogram histogram(String name, Tag... tags) { MetricKey key = new MetricKey(name, tags); return taggedMetrics.computeIfAbsent(key, k -> { String fullName = buildMetricName(name, tags); return metricRegistry.histogram(fullName); }); } private String buildMetricName(String name, Tag[] tags) { List<Tag> allTags = new ArrayList<>(); // Add static tags allTags.addAll(tagResolver.getStaticTags()); // Add dynamic tags allTags.addAll(Arrays.asList(tags)); // Add context tags allTags.addAll(tagResolver.getContextTags()); // Build name with tags return name + "[" + serializeTags(allTags) + "]"; } private String serializeTags(List<Tag> tags) { return tags.stream() .sorted(Comparator.comparing(Tag::getKey)) .map(tag -> tag.getKey() + "=" + tag.getValue()) .collect(Collectors.joining(",")); } public Map<String, Metric> getMetrics() { return taggedMetrics.entrySet().stream() .collect(Collectors.toMap( entry -> entry.getKey().toString(), Map.Entry::getValue )); } // Key class for tagged metrics private static class MetricKey { private final String name; private final List<Tag> tags; public MetricKey(String name, Tag[] tags) { this.name = name; this.tags = Arrays.asList(tags); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MetricKey metricKey = (MetricKey) o; return Objects.equals(name, metricKey.name) && Objects.equals(tags, metricKey.tags); } @Override public int hashCode() { return Objects.hash(name, tags); } @Override public String toString() { return name + tags; } } } // Tag representation public class Tag { private final String key; private final String value; public Tag(String key, String value) { this.key = key; this.value = value; } public String getKey() { return key; } public String getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Tag tag = (Tag) o; return Objects.equals(key, tag.key) && Objects.equals(value, tag.value); } @Override public int hashCode() { return Objects.hash(key, value); } public static Tag of(String key, String value) { return new Tag(key, value); } } 2. Context-Aware Tag Resolver
@Component public class TagResolver { private final List<Tag> staticTags; private final ThreadLocal<Deque<Map<String, String>>> tagContextStack; public TagResolver() { this.staticTags = new ArrayList<>(); this.tagContextStack = ThreadLocal.withInitial(ArrayDeque::new); // Initialize static tags initializeStaticTags(); } private void initializeStaticTags() { staticTags.add(Tag.of("application", "order-service")); staticTags.add(Tag.of("jvm_version", System.getProperty("java.version"))); staticTags.add(Tag.of("host", getHostName())); } public void pushTagContext() { tagContextStack.get().push(new HashMap<>()); } public void pushTagContext(Map<String, String> initialTags) { tagContextStack.get().push(new HashMap<>(initialTags)); } public void popTagContext() { Deque<Map<String, String>> stack = tagContextStack.get(); if (!stack.isEmpty()) { stack.pop(); } } public void setContextTag(String key, String value) { Deque<Map<String, String>> stack = tagContextStack.get(); if (!stack.isEmpty()) { stack.peek().put(key, value); } } public String getContextTag(String key) { Deque<Map<String, String>> stack = tagContextStack.get(); return stack.stream() .map(tags -> tags.get(key)) .filter(Objects::nonNull) .findFirst() .orElse(null); } public List<Tag> getStaticTags() { return new ArrayList<>(staticTags); } public List<Tag> getContextTags() { return tagContextStack.get().stream() .flatMap(tags -> tags.entrySet().stream()) .map(entry -> Tag.of(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } public List<Tag> getAllTags() { List<Tag> allTags = new ArrayList<>(staticTags); allTags.addAll(getContextTags()); return allTags; } private String getHostName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return "unknown"; } } } OpenTelemetry Implementation
1. Custom Metric Attributes
@Component public class OpenTelemetryMetricService { private final Meter meter; private final ContextTagManager contextTagManager; public OpenTelemetryMetricService(OpenTelemetry openTelemetry, ContextTagManager contextTagManager) { this.meter = openTelemetry.getMeter("custom.metrics"); this.contextTagManager = contextTagManager; } public void recordCounter(String name, long increment, String... tags) { Map<String, Object> attributes = buildAttributes(tags); LongCounter counter = meter.counterBuilder(name) .setDescription("Custom counter metric") .setUnit("1") .build(); counter.add(increment, Attributes.builder() .putAll(attributes) .build()); } public Timer createTimer(String name, String... tags) { Map<String, Object> attributes = buildAttributes(tags); LongHistogram histogram = meter.histogramBuilder(name + ".duration") .setDescription("Method execution duration") .setUnit("ms") .build(); return new Timer(histogram, attributes); } public ObservableGauge createObservableGauge(String name, Supplier<Number> valueSupplier, String... tags) { Map<String, Object> attributes = buildAttributes(tags); return meter.gaugeBuilder(name) .setDescription("Observable gauge") .setUnit("1") .buildWithCallback(measurement -> { measurement.record(valueSupplier.get(), Attributes.builder() .putAll(attributes) .build()); }); } private Map<String, Object> buildAttributes(String... tags) { Map<String, Object> attributes = new HashMap<>(); // Add context attributes attributes.putAll(contextTagManager.getCurrentContext()); // Add direct tags for (int i = 0; i < tags.length; i += 2) { if (i + 1 < tags.length) { attributes.put(tags[i], tags[i + 1]); } } return attributes; } public static class Timer implements AutoCloseable { private final LongHistogram histogram; private final Map<String, Object> attributes; private final long startTime; public Timer(LongHistogram histogram, Map<String, Object> attributes) { this.histogram = histogram; this.attributes = attributes; this.startTime = System.currentTimeMillis(); } @Override public void close() { long duration = System.currentTimeMillis() - startTime; histogram.record(duration, Attributes.builder() .putAll(attributes) .build()); } } } 2. Context Propagation for Tags
@Component public class ContextTagManager { private static final ContextKey<Map<String, String>> TAG_CONTEXT_KEY = ContextKey.named("metric-tags"); private final Context currentContext; public ContextTagManager() { this.currentContext = Context.current(); } public void withTags(Map<String, String> tags, Runnable operation) { Context newContext = updateContextWithTags(tags); Context previous = newContext.makeCurrent(); try { operation.run(); } finally { previous.close(); } } public <T> T withTags(Map<String, String> tags, Supplier<T> operation) { Context newContext = updateContextWithTags(tags); Context previous = newContext.makeCurrent(); try { return operation.get(); } finally { previous.close(); } } public void setTag(String key, String value) { Map<String, String> currentTags = getCurrentTags(); currentTags.put(key, value); updateContext(currentTags); } public Map<String, String> getCurrentContext() { return new HashMap<>(getCurrentTags()); } private Context updateContextWithTags(Map<String, String> newTags) { Map<String, String> currentTags = getCurrentTags(); Map<String, String> updatedTags = new HashMap<>(currentTags); updatedTags.putAll(newTags); return currentContext.with(TAG_CONTEXT_KEY, updatedTags); } private void updateContext(Map<String, String> tags) { currentContext.with(TAG_CONTEXT_KEY, tags).makeCurrent(); } @SuppressWarnings("unchecked") private Map<String, String> getCurrentTags() { Map<String, String> tags = currentContext.get(TAG_CONTEXT_KEY); return tags != null ? tags : new HashMap<>(); } } Advanced Tagging Strategies
1. Cardinality-Aware Tagging
@Component public class CardinalityAwareTagger { private final Map<String, Set<String>> tagCardinality = new ConcurrentHashMap<>(); private final int maxCardinalityPerTag = 100; public String normalizeTagValue(String tagKey, String tagValue) { if (tagValue == null) { return "unknown"; } Set<String> values = tagCardinality.computeIfAbsent( tagKey, k -> ConcurrentHashMap.newKeySet()); // If we haven't reached cardinality limit, add the value if (values.size() < maxCardinalityPerTag) { values.add(tagValue); return tagValue; } // If value is already known, use it if (values.contains(tagValue)) { return tagValue; } // Otherwise, use "other" category return "other"; } public Tag createSafeTag(String key, String value) { String safeValue = normalizeTagValue(key, value); return Tag.of(key, safeValue); } public List<Tag> createSafeTags(Map<String, String> tags) { return tags.entrySet().stream() .map(entry -> createSafeTag(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } public Map<String, Integer> getTagCardinalities() { return tagCardinality.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().size() )); } } 2. Hierarchical Tagging System
public class HierarchicalTagSystem { private final Map<String, TagNamespace> namespaces; public HierarchicalTagSystem() { this.namespaces = new ConcurrentHashMap<>(); initializeDefaultNamespaces(); } private void initializeDefaultNamespaces() { registerNamespace("infra", Arrays.asList( "cluster", "node", "zone", "region" )); registerNamespace("app", Arrays.asList( "service", "version", "instance" )); registerNamespace("business", Arrays.asList( "tenant", "user_type", "feature" )); } public void registerNamespace(String namespace, List<String> allowedKeys) { namespaces.put(namespace, new TagNamespace(namespace, allowedKeys)); } public Tag createNamespacedTag(String namespace, String key, String value) { TagNamespace ns = namespaces.get(namespace); if (ns == null) { throw new IllegalArgumentException("Unknown namespace: " + namespace); } if (!ns.isKeyAllowed(key)) { throw new IllegalArgumentException( "Key " + key + " not allowed in namespace " + namespace); } return Tag.of(namespace + "." + key, value); } public List<Tag> createNamespacedTags(Map<String, Map<String, String>> namespaceTags) { List<Tag> tags = new ArrayList<>(); namespaceTags.forEach((namespace, keyValues) -> { keyValues.forEach((key, value) -> { try { tags.add(createNamespacedTag(namespace, key, value)); } catch (IllegalArgumentException e) { // Log warning but continue System.err.println("Warning: " + e.getMessage()); } }); }); return tags; } private static class TagNamespace { private final String name; private final Set<String> allowedKeys; public TagNamespace(String name, List<String> allowedKeys) { this.name = name; this.allowedKeys = new HashSet<>(allowedKeys); } public boolean isKeyAllowed(String key) { return allowedKeys.contains(key); } } } Spring Boot Integration
1. Auto-Configuration
@Configuration @EnableConfigurationProperties(MetricTagsProperties.class) public class MetricTagsAutoConfiguration { @Bean @ConditionalOnMissingBean public TagResolver tagResolver(MetricTagsProperties properties) { TagResolver resolver = new TagResolver(); // Add properties-based static tags properties.getStaticTags().forEach(resolver::addStaticTag); return resolver; } @Bean @ConditionalOnMissingBean public DynamicTagManager dynamicTagManager(MeterRegistry meterRegistry) { return new DynamicTagManager(meterRegistry); } @Bean @ConditionalOnMissingBean public CardinalityAwareTagger cardinalityAwareTagger() { return new CardinalityAwareTagger(); } @Bean public MeterFilter metricTagFilter(CardinalityAwareTagger tagger) { return new MeterFilter() { @Override public Meter.Id map(Meter.Id id) { // Apply cardinality limits to all metrics List<Tag> safeTags = id.getTags().stream() .map(tag -> tagger.createSafeTag(tag.getKey(), tag.getValue())) .collect(Collectors.toList()); return id.replaceTags(safeTags); } }; } } @ConfigurationProperties(prefix = "metrics.tags") public class MetricTagsProperties { private Map<String, String> staticTags = new HashMap<>(); private Cardinality cardinality = new Cardinality(); // Getters and setters public Map<String, String> getStaticTags() { return staticTags; } public void setStaticTags(Map<String, String> staticTags) { this.staticTags = staticTags; } public Cardinality getCardinality() { return cardinality; } public void setCardinality(Cardinality cardinality) { this.cardinality = cardinality; } public static class Cardinality { private int maxPerTag = 100; private boolean enabled = true; // Getters and setters public int getMaxPerTag() { return maxPerTag; } public void setMaxPerTag(int maxPerTag) { this.maxPerTag = maxPerTag; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } } 2. Application Configuration
# application.yml metrics: tags: static-tags: application: order-service environment: ${ENVIRONMENT:dev} version: 1.0.0 region: ${AWS_REGION:us-east-1} cardinality: max-per-tag: 50 enabled: true management: metrics: export: prometheus: enabled: true tags: application: order-service environment: ${ENVIRONMENT:dev} Testing Custom Metric Tags
1. Test Utilities
@ExtendWith(MockitoExtension.class) public class MetricTagTest { @Test public void testDynamicTagContext() { try (DynamicTagManager tagManager = new DynamicTagManager(meterRegistry)) { tagManager.setContextTag("user_id", "12345"); tagManager.setContextTag("tenant", "acme"); Counter counter = tagManager.counterWithContext("api.requests"); counter.increment(); // Verify tags are applied Meter meter = meterRegistry.find("api.requests").counter(); assertThat(meter).isNotNull(); assertThat(meter.getId().getTag("user_id")).isEqualTo("12345"); assertThat(meter.getId().getTag("tenant")).isEqualTo("acme"); } } @Test public void testCardinalityLimiting() { CardinalityAwareTagger tagger = new CardinalityAwareTagger(); for (int i = 0; i < 150; i++) { String value = tagger.normalizeTagValue("user_id", "user_" + i); if (i < 100) { assertThat(value).isEqualTo("user_" + i); } else { assertThat(value).isEqualTo("other"); } } } @SpringBootTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class IntegrationTest { @Autowired private MeterRegistry meterRegistry; @Autowired private DynamicTagManager tagManager; @Test public void testMetricTagsInWebRequest() { // Test that web requests automatically get tagged webTestClient.get().uri("/api/orders") .header("X-User-Id", "test-user") .header("X-Tenant-Id", "test-tenant") .exchange() .expectStatus().isOk(); Timer timer = meterRegistry.find("http.requests") .tag("user_id", "test-user") .tag("tenant", "test-tenant") .timer(); assertThat(timer).isNotNull(); assertThat(timer.count()).isGreaterThan(0); } } } Performance Considerations
1. Tag Creation Optimization
public class TagPool { private final Map<String, Map<String, Tag>> tagCache = new ConcurrentHashMap<>(); public Tag getTag(String key, String value) { return tagCache .computeIfAbsent(key, k -> new ConcurrentHashMap<>()) .computeIfAbsent(value, v -> Tag.of(key, value)); } public List<Tag> getTags(Map<String, String> tagMap) { return tagMap.entrySet().stream() .map(entry -> getTag(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } } @Component public class HighPerformanceTagger { private final TagPool tagPool; private final CardinalityAwareTagger cardinalityTagger; public HighPerformanceTagger(TagPool tagPool, CardinalityAwareTagger cardinalityTagger) { this.tagPool = tagPool; this.cardinalityTagger = cardinalityTagger; } public List<Tag> createOptimizedTags(Map<String, String> rawTags) { Map<String, String> safeTags = new HashMap<>(); for (Map.Entry<String, String> entry : rawTags.entrySet()) { String safeValue = cardinalityTagger.normalizeTagValue( entry.getKey(), entry.getValue()); safeTags.put(entry.getKey(), safeValue); } return tagPool.getTags(safeTags); } } Conclusion
Custom metric tags in Java provide powerful dimensional analysis capabilities when implemented correctly. Key takeaways:
- Use appropriate tagging strategies based on your monitoring framework
- Manage tag cardinality to prevent performance issues
- Implement context propagation for consistent tagging across operations
- Leverage dependency injection for clean integration
- Monitor tag usage and performance impact
By following these patterns, you can create robust, performant metric tagging systems that provide deep insights into your application's behavior while maintaining system stability.