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();
}
}