diff --git a/build.gradle b/build.gradle index abbe17be9..b2a867332 100644 --- a/build.gradle +++ b/build.gradle @@ -6,18 +6,15 @@ apply plugin: 'idea' apply plugin: 'com.github.johnrengelman.shadow' configurations.all { - // check for updates every build + // check for updates every build for dependencies with: 'changing: true' resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } repositories { - mavenCentral() mavenLocal() - // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: - maven { - url "https://oss.sonatype.org/content/groups/public/" - } + maven { url "https://oss.sonatype.org/content/groups/public/" } + mavenCentral() } allprojects { @@ -38,7 +35,7 @@ dependencies { compile "redis.clients:jedis:2.8.0" testCompile "org.easymock:easymock:3.4" testCompile 'junit:junit:[4.10,)' - testRuntime "org.slf4j:slf4j-simple:1.7.7" + testRuntime "ch.qos.logback:logback-classic:1.1.3" } jar { diff --git a/src/main/java/com/launchdarkly/client/EventProcessor.java b/src/main/java/com/launchdarkly/client/EventProcessor.java index 8243d70d7..ab4eb1362 100644 --- a/src/main/java/com/launchdarkly/client/EventProcessor.java +++ b/src/main/java/com/launchdarkly/client/EventProcessor.java @@ -1,5 +1,6 @@ package com.launchdarkly.client; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.gson.Gson; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; @@ -14,22 +15,34 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Random; import java.util.concurrent.*; class EventProcessor implements Closeable { - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); + private final ScheduledExecutorService scheduler; + private final Random random = new Random(); private final BlockingQueue queue; private final String apiKey; + private final LDConfig config; private final Consumer consumer; EventProcessor(String apiKey, LDConfig config) { this.apiKey = apiKey; this.queue = new ArrayBlockingQueue<>(config.capacity); this.consumer = new Consumer(config); + this.config = config; + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("LaunchDarkly-EventProcessor-%d") + .build(); + this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory); this.scheduler.scheduleAtFixedRate(consumer, 0, config.flushInterval, TimeUnit.SECONDS); } boolean sendEvent(Event e) { + if (config.samplingInterval > 0 && random.nextInt(config.samplingInterval) != 0) { + return true; + } return queue.offer(e); } @@ -43,18 +56,8 @@ public void flush() { this.consumer.flush(); } - static class DaemonThreadFactory implements ThreadFactory { - public Thread newThread(Runnable r) { - Thread thread = new Thread(r); - thread.setDaemon(true); - return thread; - } - } - class Consumer implements Runnable { private final Logger logger = LoggerFactory.getLogger(Consumer.class); - - private final CloseableHttpClient client; private final LDConfig config; @@ -78,6 +81,7 @@ public void flush() { } private void postEvents(List events) { + logger.debug("Posting " + events.size() + " event(s).."); CloseableHttpResponse response = null; Gson gson = new Gson(); String json = gson.toJson(events); @@ -95,13 +99,11 @@ private void postEvents(List events) { if (status >= 300) { if (status == HttpStatus.SC_UNAUTHORIZED) { logger.error("Invalid API key"); - } - else { + } else { logger.error("Unexpected status code: " + status); } - } - else { - logger.debug("Successfully processed events"); + } else { + logger.debug("Successfully posted " + events.size() + " event(s)."); } } catch (IOException e) { logger.error("Unhandled exception in LaunchDarkly client attempting to connect to URI: " + config.eventsURI, e); @@ -112,7 +114,6 @@ private void postEvents(List events) { logger.error("Unhandled exception in LaunchDarkly client", e); } } - } } } diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index 6abca54a4..8fbdce5cf 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -23,6 +23,7 @@ public final class LDConfig { private static final int DEFAULT_FLUSH_INTERVAL = 5; private static final long DEFAULT_POLLING_INTERVAL_MILLIS = 1000L; private static final long DEFAULT_START_WAIT_MILLIS = 0L; + private static final int DEFAULT_SAMPLING_INTERVAL = 0; private static final Logger logger = LoggerFactory.getLogger(LDConfig.class); protected static final LDConfig DEFAULT = new Builder().build(); @@ -42,6 +43,7 @@ public final class LDConfig { final boolean offline; final long pollingIntervalMillis; final long startWaitMillis; + final int samplingInterval; protected LDConfig(Builder builder) { this.baseURI = builder.baseURI; @@ -63,6 +65,7 @@ protected LDConfig(Builder builder) { this.pollingIntervalMillis = builder.pollingIntervalMillis; } this.startWaitMillis = builder.startWaitMillis; + this.samplingInterval = builder.samplingInterval; } /** @@ -94,6 +97,7 @@ public static class Builder { private long pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; private FeatureStore featureStore = new InMemoryFeatureStore(); public long startWaitMillis = DEFAULT_START_WAIT_MILLIS; + public int samplingInterval = DEFAULT_SAMPLING_INTERVAL; /** * Creates a builder with all configuration parameters set to the default @@ -335,6 +339,21 @@ public Builder startWaitMillis(long startWaitMillis) { return this; } + /** + * Enable event sampling. When set to the default of zero, sampling is disabled and all events + * are sent back to LaunchDarkly. When set to greater than zero, there is a 1 in + * samplingInterval chance events will be will be sent. + * + *

Example: if you want 5% sampling rate, set samplingInterval to 20. + * + * @param samplingInterval the sampling interval. + * @return the builder + */ + public Builder samplingInterval(int samplingInterval) { + this.samplingInterval = samplingInterval; + return this; + } + HttpHost proxyHost() { if (this.proxyHost == null && this.proxyPort == -1 && this.proxyScheme == null) { return null; diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java index c8bfa4a02..3b1541715 100644 --- a/src/main/java/com/launchdarkly/client/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -1,13 +1,11 @@ package com.launchdarkly.client; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; public class PollingProcessor implements UpdateProcessor { @@ -40,7 +38,10 @@ public Future start() { logger.info("Starting LaunchDarkly polling client with interval: " + config.pollingIntervalMillis + " milliseconds"); final VeryBasicFuture initFuture = new VeryBasicFuture(); - scheduler = Executors.newScheduledThreadPool(1); + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("LaunchDarkly-PollingProcessor-%d") + .build(); + scheduler = Executors.newScheduledThreadPool(1, threadFactory); scheduler.scheduleAtFixedRate(new Runnable() { @Override diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index e5ef339b7..cb9d90aca 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -104,11 +104,10 @@ else if (name.equals(INDIRECT_PATCH)) { @Override public void onError(Throwable throwable) { - logger.error("Encountered exception in LaunchDarkly client: " + throwable.getMessage()); + logger.warn("Encountered EventSource error", throwable); } }; - es = new EventSource.Builder(handler, URI.create(config.streamURI.toASCIIString() + "/features")) .headers(headers) .build(); diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 000000000..757bb2429 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}:%line - %msg%n + + + + + + + diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties deleted file mode 100644 index 413b5640e..000000000 --- a/src/test/resources/simplelogger.properties +++ /dev/null @@ -1,34 +0,0 @@ -# SLF4J's SimpleLogger configuration file -# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. - -# Default logging detail level for all instances of SimpleLogger. -# Must be one of ("trace", "debug", "info", "warn", or "error"). -# If not specified, defaults to "info". -org.slf4j.simpleLogger.defaultLogLevel=info - -# Logging detail level for a SimpleLogger instance named "xxxxx". -# Must be one of ("trace", "debug", "info", "warn", or "error"). -# If not specified, the default logging detail level is used. -#org.slf4j.simpleLogger.log.xxxxx= - -# Set to true if you want the current date and time to be included in output messages. -# Default is false, and will output the number of milliseconds elapsed since startup. -#org.slf4j.simpleLogger.showDateTime=false - -# The date and time format to be used in the output messages. -# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. -# If the format is not specified or is invalid, the default format is used. -# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. -#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z - -# Set to true if you want to output the current thread name. -# Defaults to true. -#org.slf4j.simpleLogger.showThreadName=true - -# Set to true if you want the Logger instance name to be included in output messages. -# Defaults to true. -#org.slf4j.simpleLogger.showLogName=true - -# Set to true if you want the last component of the name to be included in output messages. -# Defaults to false. -org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file