Custom Metric Tags in Java: Advanced Implementation Guide

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:

  1. Use appropriate tagging strategies based on your monitoring framework
  2. Manage tag cardinality to prevent performance issues
  3. Implement context propagation for consistent tagging across operations
  4. Leverage dependency injection for clean integration
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper