package tc.oc.analytics.datadog; import java.util.Collection; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableSet; import com.google.inject.OutOfScopeException; import com.timgroup.statsd.StatsDClient; import tc.oc.analytics.AnalyticsClient; import tc.oc.analytics.Event; import tc.oc.analytics.Tag; import tc.oc.analytics.Tagger; import tc.oc.commons.core.inject.Injection; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.util.CacheUtils; import tc.oc.minecraft.suspend.Suspendable; class DataDogClient implements AnalyticsClient, Suspendable { private final Logger logger; private final DataDogConfig config; private final Provider<StatsDClient> clientProvider; private @Nullable StatsDClient client; // Provision taggers at the moment the tags are used, so they can be scoped. // We also use a provider for the entire collection to avoid circular deps. private final Provider<Collection<Provider<Tagger>>> taggers; private final LoadingCache<Tag, String> tagCache = CacheUtils.newWeakKeyCache( tag -> tag.name() + ":" + tag.value() ); private final LoadingCache<ImmutableSet<Tag>, String> tagSetCache = CacheUtils.newWeakKeyCache( tags -> tags.stream() .map(tagCache::getUnchecked) .collect(Collectors.joining(",")) ); @Inject DataDogClient(Loggers loggers, DataDogConfig config, Provider<StatsDClient> clientProvider, Provider<Collection<Provider<Tagger>>> taggers) { this.logger = loggers.get(getClass()); this.config = config; this.clientProvider = clientProvider; this.taggers = taggers; this.client = clientProvider.get(); } @Override public boolean isActive() { return config.enabled(); } private static final String[] EMPTY = new String[]{}; String[] renderedTags() { final StringBuilder sb = new StringBuilder(); boolean some = false; for(Provider<Tagger> provider : taggers.get()) { final Tagger tagger; try { tagger = Injection.unwrappingExceptions(OutOfScopeException.class, provider); } catch(OutOfScopeException e) { // If the tagger is out of scope, just omit its tags, // but log a warning in case this hides an unexpected exception. logger.warning("Ignoring out-of-scope tagger (" + e.toString() + ")"); continue; } final ImmutableSet<Tag> tags = tagger.tags(); if(!tags.isEmpty()) { if(some) sb.append(','); some = true; sb.append(tagSetCache.getUnchecked(tags)); } } return some ? new String[] {sb.toString()} : EMPTY; } @Override public void count(String metric, int quantity) { if(client == null) return; client.count(metric, quantity, renderedTags()); } @Override public void measure(String metric, double value) { if(client == null) return; client.gauge(metric, value, renderedTags()); } @Override public void sample(String metric, double value) { if(client == null) return; client.histogram(metric, value, renderedTags()); } @Override public void event(Event event) { if(client == null) return; client.recordEvent( com.timgroup.statsd.Event.builder() .withAlertType(alertType(event.level())) .withAggregationKey(event.key()) .withTitle(event.title()) .withText(event.body()) .build() ); } private static com.timgroup.statsd.Event.AlertType alertType(Event.Level level) { switch(level) { case SUCCESS: return com.timgroup.statsd.Event.AlertType.SUCCESS; case WARNING: return com.timgroup.statsd.Event.AlertType.WARNING; case ERROR: return com.timgroup.statsd.Event.AlertType.ERROR; default: return com.timgroup.statsd.Event.AlertType.INFO; } } @Override public void suspend() { client.stop(); client = null; } @Override public void resume() { client = clientProvider.get(); } }