Springboot2 custom statsd indicator prefix

  springboot

Order

This paper mainly studies the springboot2 custom statsd index prefix

Background

Springboot2 introduces micrometer. Version 1.x of Spring.metrics.export.statsd.prefix has been marked as obsolete in version 2, but version 2 does not give corresponding configuration items.

FlavorStatsdLineBuilder

micrometer-registry-statsd-1.0.1-sources.jar! /io/micrometer/statsd/internal/FlavorStatsdLineBuilder.java

/**
 * A Statsd serializer for a particular {@link Meter} that formats the line in different
 * ways depending on the prevailing {@link StatsdFlavor}.
 *
 * @author Jon Schneider
 */
public class FlavorStatsdLineBuilder implements StatsdLineBuilder {
    private final Meter.Id id;
    private final StatsdFlavor flavor;
    private final HierarchicalNameMapper nameMapper;
    private final MeterRegistry.Config config;

    private final Function<NamingConvention, String> datadogTagString;
    private final Function<NamingConvention, String> telegrafTagString;

    public FlavorStatsdLineBuilder(Meter.Id id, StatsdFlavor flavor, HierarchicalNameMapper nameMapper, MeterRegistry.Config config) {
        this.id = id;
        this.flavor = flavor;
        this.nameMapper = nameMapper;
        this.config = config;

        // service:payroll,region:us-west
        this.datadogTagString = memoize(convention ->
                id.getTags().iterator().hasNext() ?
                        id.getConventionTags(convention).stream()
                                .map(t -> t.getKey() + ":" + t.getValue())
                                .collect(Collectors.joining(","))
                        : null
        );

        // service=payroll,region=us-west
        this.telegrafTagString = memoize(convention ->
                id.getTags().iterator().hasNext() ?
                        id.getConventionTags(convention).stream()
                                .map(t -> t.getKey() + "=" + t.getValue())
                                .collect(Collectors.joining(","))
                        : null
        );
    }

    @Override
    public String count(long amount, Statistic stat) {
        return line(Long.toString(amount), stat, "c");
    }

    @Override
    public String gauge(double amount, Statistic stat) {
        return line(DoubleFormat.decimalOrNan(amount), stat, "g");
    }

    @Override
    public String histogram(double amount) {
        return line(DoubleFormat.decimalOrNan(amount), null, "h");
    }

    @Override
    public String timing(double timeMs) {
        return line(DoubleFormat.decimalOrNan(timeMs), null, "ms");
    }

    private String line(String amount, @Nullable Statistic stat, String type) {
        switch (flavor) {
            case ETSY:
                return metricName(stat) + ":" + amount + "|" + type;
            case DATADOG:
                return metricName(stat) + ":" + amount + "|" + type + tags(stat, datadogTagString.apply(config.namingConvention()),":", "|#");
            case TELEGRAF:
            default:
                return metricName(stat) + tags(stat, telegrafTagString.apply(config.namingConvention()),"=", ",") + ":" + amount + "|" + type;
        }
    }

    private String tags(@Nullable Statistic stat, String otherTags, String keyValueSeparator, String preamble) {
        String tags = of(stat == null ? null : "statistic" + keyValueSeparator + stat.getTagValueRepresentation(), otherTags)
                .filter(Objects::nonNull)
                .collect(Collectors.joining(","));

        if(!tags.isEmpty())
            tags = preamble + tags;
        return tags;
    }

    private String metricName(@Nullable Statistic stat) {
        switch (flavor) {
            case ETSY:
                return nameMapper.toHierarchicalName(stat != null ? id.withTag(stat) : id, config.namingConvention());
            case DATADOG:
            case TELEGRAF:
            default:
                return config.namingConvention().name(id.getName(), id.getType(), id.getBaseUnit());
        }
    }
}

You can see that the line method is called inside the count, gauge, histogram, and timing methods, while the line method calls metricName to construct the index name, while metricName calls the HierarchicalNameMapper’s toHierarchicalName method (Flavor is ESTY.)

HierarchicalNameMapper

micrometer-core-1.0.1-sources.jar! /io/micrometer/core/instrument/util/HierarchicalNameMapper.java

/**
 * Defines the mapping between a combination of name + dimensional tags and a hierarchical name.
 *
 * @author Jon Schneider
 */
public interface HierarchicalNameMapper {
    /**
     * Sort tags alphabetically by key and append tag key values to the name with '.', e.g.
     * {@code http_server_requests.response.200.method.GET}
     */
    HierarchicalNameMapper DEFAULT = (id, convention) -> {
        String tags = "";

        if (id.getTags().iterator().hasNext()) {
            tags = "." + id.getConventionTags(convention).stream()
                .map(t -> t.getKey() + "." + t.getValue())
                .map(nameSegment -> nameSegment.replace(" ", "_"))
                .collect(Collectors.joining("."));
        }

        return id.getConventionName(convention) + tags;
    };

    String toHierarchicalName(Meter.Id id, NamingConvention convention);
}

The HierarchicalNameMapper interface defines a DEFAULT implementation, which is used by DEFAULT in StatsdmetricsExportAutoConfiguration.

  • StatsdMetricsExportAutoConfiguration

spring-boot-actuator-autoconfigure-2.0.0.RELEASE-sources.jar! /org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfiguration.java

    @Bean
    @ConditionalOnMissingBean
    public HierarchicalNameMapper hierarchicalNameMapper() {
        return HierarchicalNameMapper.DEFAULT;
    }

custom

The prefix of statsd indicator can be customized by customizing HierarchicalNameMapper, as shown in the following example

    @Bean
    public HierarchicalNameMapper hierarchicalNameMapper() {
        return new HierarchicalNameMapper(){

            @Override
            public String toHierarchicalName(Meter.Id id, NamingConvention convention) {
                String tags = "";

                if (id.getTags().iterator().hasNext()) {
                    tags = "." + id.getConventionTags(convention).stream()
                            .map(t -> t.getKey() + "." + t.getValue())
                            .map(nameSegment -> nameSegment.replace(" ", "_"))
                            .collect(Collectors.joining("."));
                }

                return "demo." + id.getConventionName(convention) + tags;
            }
        };
    }

The DEFAULT method has been modified here and a demo has been added to return as a prefix, thus completing the task.

Summary

Springboot2 currently does not directly support prefix specifying statsd through configuration files, but it can be implemented by customizing HierarchicalNameMapper with a little code.

doc