diff --git a/src/main/java/com/stackify/api/common/ApiConfiguration.java b/src/main/java/com/stackify/api/common/ApiConfiguration.java index 1af72fa..2e0c6fb 100644 --- a/src/main/java/com/stackify/api/common/ApiConfiguration.java +++ b/src/main/java/com/stackify/api/common/ApiConfiguration.java @@ -23,32 +23,32 @@ * @author Eric Martin */ public class ApiConfiguration { - + /** * Default API URL */ private static final String DEFAULT_API_URL = "https://api.stackify.com"; - + /** * API URL */ private final String apiUrl; - + /** * API Key */ private final String apiKey; - + /** * Application name */ private final String application; - + /** * Environment */ private final String environment; - + /** * Environment details */ @@ -93,11 +93,11 @@ public EnvironmentDetail getEnvDetail() { * @param builder The Builder object that contains all of the values for initialization */ private ApiConfiguration(final Builder builder) { - this.apiUrl = builder.apiUrl; - this.apiKey = builder.apiKey; - this.application = builder.application; - this.environment = builder.environment; - this.envDetail = builder.envDetail; + this.apiUrl = builder.apiUrl; + this.apiKey = builder.apiKey; + this.application = builder.application; + this.environment = builder.environment; + this.envDetail = builder.envDetail; } /** @@ -107,6 +107,19 @@ public static Builder newBuilder() { return new Builder(); } + + /** + * @return a Builder object based on current instance + */ + public Builder toBuilder() { + return newBuilder() + .apiUrl(apiUrl) + .apiKey(apiKey) + .application(application) + .environment(environment) + .envDetail(envDetail); + } + /** * ApiConfiguration.Builder separates the construction of a ApiConfiguration from its representation */ @@ -116,82 +129,82 @@ public static class Builder { * The builder's apiUrl */ private String apiUrl; - + /** * The builder's apiKey */ private String apiKey; - + /** * The builder's application */ private String application; - + /** * The builder's environment */ private String environment; - + /** * The builder's envDetail */ private EnvironmentDetail envDetail; - + /** * Sets the builder's apiUrl * @param apiUrl The apiUrl to be set * @return Reference to the current object */ public Builder apiUrl(final String apiUrl) { - this.apiUrl = apiUrl; - return this; + this.apiUrl = apiUrl; + return this; } - + /** * Sets the builder's apiKey * @param apiKey The apiKey to be set * @return Reference to the current object */ public Builder apiKey(final String apiKey) { - this.apiKey = apiKey; - return this; + this.apiKey = apiKey; + return this; } - + /** * Sets the builder's application * @param application The application to be set * @return Reference to the current object */ public Builder application(final String application) { - this.application = application; - return this; + this.application = application; + return this; } - + /** * Sets the builder's environment * @param environment The environment to be set * @return Reference to the current object */ public Builder environment(final String environment) { - this.environment = environment; - return this; + this.environment = environment; + return this; } - + /** * Sets the builder's envDetail * @param envDetail The envDetail to be set * @return Reference to the current object */ public Builder envDetail(final EnvironmentDetail envDetail) { - this.envDetail = envDetail; - return this; + this.envDetail = envDetail; + return this; } - + /** * @return A new object constructed from this builder */ public ApiConfiguration build() { - return new ApiConfiguration(this); + return new ApiConfiguration(this); } } @@ -201,13 +214,13 @@ public ApiConfiguration build() { */ @Override public String toString() { - return Objects.toStringHelper(this) - .omitNullValues() - .add("apiUrl", apiUrl) - .add("apiKey", apiKey) - .add("application", application) - .add("environment", environment) - .add("envDetail", envDetail) - .toString(); + return Objects.toStringHelper(this) + .omitNullValues() + .add("apiUrl", apiUrl) + .add("apiKey", apiKey) + .add("application", application) + .add("environment", environment) + .add("envDetail", envDetail) + .toString(); } } diff --git a/src/main/java/com/stackify/api/common/AppIdentityService.java b/src/main/java/com/stackify/api/common/AppIdentityService.java index a60170c..e73dbe4 100644 --- a/src/main/java/com/stackify/api/common/AppIdentityService.java +++ b/src/main/java/com/stackify/api/common/AppIdentityService.java @@ -16,7 +16,10 @@ package com.stackify.api.common; import java.io.IOException; +import java.util.Hashtable; +import java.util.Map; +import com.stackify.api.EnvironmentDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,27 +47,22 @@ public class AppIdentityService { * Five minutes (in milliseconds) */ private static long FIVE_MINUTES_MILLIS = 300000; - - /** - * Timestamp of the last query - */ - private long lastQuery = 0; - + /** - * The cached app identity + * Map The cached app identity */ - private Optional appIdentity = Optional.absent(); - + private Map applicationIdentityCache = new Hashtable(); + /** * The API configuration */ - private final ApiConfiguration apiConfig; + private final ApiConfiguration defaultApiConfig; /** * Jackson object mapper */ private final ObjectMapper objectMapper; - + /** * Constructor * @param apiConfig The API configuration @@ -73,55 +71,161 @@ public class AppIdentityService { public AppIdentityService(final ApiConfiguration apiConfig, final ObjectMapper objectMapper) { Preconditions.checkNotNull(apiConfig); Preconditions.checkNotNull(objectMapper); - - this.apiConfig = apiConfig; + + this.defaultApiConfig = apiConfig; this.objectMapper = objectMapper; } - + + + /** * Retrieves the application identity given the environment details * @return The application identity */ - public Optional getAppIdentity() { - if (!appIdentity.isPresent()) { - long currentTimeMillis = System.currentTimeMillis(); - - if (lastQuery + FIVE_MINUTES_MILLIS < currentTimeMillis) { - try { - lastQuery = currentTimeMillis; - appIdentity = Optional.fromNullable(identifyApp()); - LOGGER.debug("Application identity: {}", appIdentity.get()); - } catch (Throwable t) { - LOGGER.info("Unable to determine application identity", t); - } + private Optional getAppIdentity(ApiConfiguration apiConfig) { + final String applicationName = apiConfig.getApplication(); + + if (applicationName == null) + return Optional.absent(); + + // If there's no record create it. + if (!applicationIdentityCache.containsKey(applicationName)) { + applicationIdentityCache.put(applicationName, new AppIdentityState()); + } + + final AppIdentityState state = applicationIdentityCache.get(applicationName); + final long now = System.currentTimeMillis(); + + if ((state.lastModified() + FIVE_MINUTES_MILLIS) < now) { + state.touch(); + try { + final AppIdentity identity = identifyApp(apiConfig); + applicationIdentityCache.put(applicationName, state.updateAppIdentity(identity)); + + LOGGER.debug("Application identity: {}", identity); + + } catch (Throwable t) { + LOGGER.info("Unable to determine application identity", t); } } - - return appIdentity; + + return applicationIdentityCache.get(apiConfig.getApplication()).getAppIdentity(); } - + + + /** + * Retrieves the application identity given the environment details + * @param applicationName - name of the application + * @return The application identity + */ + public Optional getAppIdentity(final String applicationName) { + if (isCached(applicationName)) { + return applicationIdentityCache.get(applicationName).getAppIdentity(); + + } else { + // Update environment detail with new configured application name + final EnvironmentDetail updatedEnvDetail = + updateEnvironmentDetail(defaultApiConfig.getEnvDetail(), applicationName); + + // use existing apiConfig, with new application name + final ApiConfiguration updatedApiConfig = defaultApiConfig.toBuilder() + .application(applicationName) + .envDetail(updatedEnvDetail) + .build(); + + return getAppIdentity(updatedApiConfig); + } + } + + + public Optional getAppIdentity() { + if (isCached(defaultApiConfig.getApplication())) + return applicationIdentityCache.get(defaultApiConfig.getApplication()).getAppIdentity(); + else + return getAppIdentity(defaultApiConfig); + } + /** * Retrieves the application identity given the environment details * @return The application identity * @throws IOException - * @throws HttpException + * @throws HttpException */ - private AppIdentity identifyApp() throws IOException, HttpException { - + private AppIdentity identifyApp(ApiConfiguration apiConfig) throws IOException, HttpException { // convert to json bytes - byte[] jsonBytes = objectMapper.writer().writeValueAsBytes(apiConfig.getEnvDetail()); - + // post to stackify - - HttpClient httpClient = new HttpClient(apiConfig); - String responseString = httpClient.post("/Metrics/IdentifyApp", jsonBytes); - + final HttpClient httpClient = new HttpClient(apiConfig); + final String responseString = httpClient.post("/Metrics/IdentifyApp", jsonBytes); + // deserialize the response and return the app identity - ObjectReader jsonReader = objectMapper.reader(new TypeReference(){}); - AppIdentity appIdentity = jsonReader.readValue(responseString); - - return appIdentity; + return jsonReader.readValue(responseString); + } + + + private boolean isCached(final String applicationName) { + Preconditions.checkNotNull(applicationName); + + return + applicationIdentityCache.containsKey(applicationName) && + applicationIdentityCache.get(applicationName).getAppIdentity().isPresent(); + } + + + private EnvironmentDetail updateEnvironmentDetail(final EnvironmentDetail envDetail, final String newConfAppName) { + return EnvironmentDetail.newBuilder() + .deviceName(envDetail.getDeviceName()) + .appName(envDetail.getAppName()) + .appLocation(envDetail.getAppLocation()) + .configuredAppName(newConfAppName) + .configuredEnvironmentName(envDetail.getConfiguredEnvironmentName()) + .build(); + } + + + /** + * This class contains appIdentity and it's modification date + */ + private class AppIdentityState { + + private Optional mayBeAppIdentity = Optional.absent(); + private long lastQueryTimeStamp; + + public AppIdentityState() { + this.lastQueryTimeStamp = 0; + this.mayBeAppIdentity = Optional.absent(); + } + + public AppIdentityState(final AppIdentity appIdentity) { + this.lastQueryTimeStamp = 0; + this.mayBeAppIdentity = Optional.fromNullable(appIdentity); + } + + public AppIdentityState(final AppIdentity appIdentity, long timestamp) { + this.lastQueryTimeStamp = timestamp; + this.mayBeAppIdentity = Optional.fromNullable(appIdentity); + } + + public final AppIdentityState updateAppIdentity(final AppIdentity appIdentity) { + mayBeAppIdentity = Optional.fromNullable(appIdentity); + return this; + } + + public final Optional getAppIdentity() { + return mayBeAppIdentity; + } + + public final long lastModified() { + return lastQueryTimeStamp; + } + + /** + * Changes last modified date + */ + public final void touch() { + this.lastQueryTimeStamp = System.currentTimeMillis(); + } } } diff --git a/src/main/java/com/stackify/api/common/log/LogCollector.java b/src/main/java/com/stackify/api/common/log/LogCollector.java index fe5bb07..317b535 100644 --- a/src/main/java/com/stackify/api/common/log/LogCollector.java +++ b/src/main/java/com/stackify/api/common/log/LogCollector.java @@ -37,31 +37,31 @@ * @author Eric Martin */ public class LogCollector { - + /** * Max batch size of log messages to be sent in a single request */ private static final int MAX_BATCH = 100; - + /** * The logger (project) name */ private final String logger; - + /** * Environment details */ private final EnvironmentDetail envDetail; - + /** * Application identity service */ private final AppIdentityService appIdentityService; - + /** * The queue of objects to be transmitted */ - private final Queue queue = Queues.synchronizedQueue(EvictingQueue.create(10000)); + private final Queue queue = Queues.synchronizedQueue(EvictingQueue.create(10000)); /** * Constructor @@ -77,88 +77,98 @@ public LogCollector(final String logger, final EnvironmentDetail envDetail, fina this.envDetail = envDetail; this.appIdentityService = appIdentityService; } - + /** * Queues logMsg to be sent * @param logMsg The log message */ public void addLogMsg(final LogMsg logMsg) { - Preconditions.checkNotNull(logMsg); + Preconditions.checkNotNull(logMsg); queue.offer(logMsg); } - + /** * Flushes the queue by sending all messages to Stackify * @param sender The LogMsgGroup sender * @return The number of messages sent to Stackify * @throws IOException - * @throws HttpException + * @throws HttpException */ public int flush(final LogSender sender) throws IOException, HttpException { int numSent = 0; int maxToSend = queue.size(); - + if (0 < maxToSend) { - Optional appIdentity = appIdentityService.getAppIdentity(); - + while (numSent < maxToSend) { - + // get the next batch of messages - int batchSize = Math.min(maxToSend - numSent, MAX_BATCH); - + List batch = Lists.newArrayListWithCapacity(batchSize); - + for (int i = 0; i < batchSize; ++i) { batch.add(queue.remove()); } - + // build the log message group - - LogMsgGroup.Builder groupBuilder = LogMsgGroup.newBuilder(); - - groupBuilder.platform("java"); - groupBuilder.logger(logger); - groupBuilder.serverName(envDetail.getDeviceName()); - groupBuilder.env(envDetail.getConfiguredEnvironmentName()); - groupBuilder.appName(envDetail.getConfiguredAppName()); - groupBuilder.appLoc(envDetail.getAppLocation()); - - if (appIdentity.isPresent()) { - groupBuilder.cdId(appIdentity.get().getDeviceId()); - groupBuilder.cdAppId(appIdentity.get().getDeviceAppId()); - groupBuilder.appNameId(appIdentity.get().getAppNameId()); - groupBuilder.appEnvId(appIdentity.get().getAppEnvId()); - groupBuilder.envId(appIdentity.get().getEnvId()); - groupBuilder.env(appIdentity.get().getEnv()); - - if ((appIdentity.get().getAppName() != null) && (0 < appIdentity.get().getAppName().length())) { - groupBuilder.appName(appIdentity.get().getAppName()); - } - } - - groupBuilder.msgs(batch); - - LogMsgGroup group = groupBuilder.build(); - + LogMsgGroup group = createLogMessageGroup(batch, logger, envDetail, appIdentity); + // send the batch to Stackify - - int httpStatus = sender.send(group); - + int httpStatus = sender.send(group); + // if the batch failed to transmit, return the appropriate transmission status - if (httpStatus != HttpURLConnection.HTTP_OK) { throw new HttpException(httpStatus); } - + // next iteration - numSent += batchSize; } } - + return numSent; } + + /** + * + * @param batch - a bunch of messages that should be sent over the wire + * @param logger - logger (project) name + * @param envDetail - environment details + * @param appIdentity - application identity + * @return LogMessage group object with + */ + private LogMsgGroup createLogMessageGroup ( + final List batch, final String logger, final EnvironmentDetail envDetail, final Optional appIdentity + ) { + final LogMsgGroup.Builder groupBuilder = LogMsgGroup.newBuilder(); + + groupBuilder + .platform("java") + .logger(logger) + .serverName(envDetail.getDeviceName()) + .env(envDetail.getConfiguredEnvironmentName()) + .appName(envDetail.getConfiguredAppName()) + .appLoc(envDetail.getAppLocation()); + + if (appIdentity.isPresent()) { + groupBuilder + .cdId(appIdentity.get().getDeviceId()) + .cdAppId(appIdentity.get().getDeviceAppId()) + .appNameId(appIdentity.get().getAppNameId()) + .appEnvId(appIdentity.get().getAppEnvId()) + .envId(appIdentity.get().getEnvId()) + .env(appIdentity.get().getEnv()); + + if ((appIdentity.get().getAppName() != null) && (0 < appIdentity.get().getAppName().length())) { + groupBuilder.appName(appIdentity.get().getAppName()); + } + } + + groupBuilder.msgs(batch); + + return groupBuilder.build(); + } } diff --git a/src/test/java/com/stackify/api/common/AppIdentityServiceTest.java b/src/test/java/com/stackify/api/common/AppIdentityServiceTest.java index bf16195..6e39675 100644 --- a/src/test/java/com/stackify/api/common/AppIdentityServiceTest.java +++ b/src/test/java/com/stackify/api/common/AppIdentityServiceTest.java @@ -20,6 +20,7 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; +import static org.powermock.api.mockito.PowerMockito.*; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -39,29 +40,32 @@ public class AppIdentityServiceTest { /** * testGetAppIdentity - * @throws Exception + * @throws Exception */ @Test - public void testGetAppIdentity() throws Exception { + public void testGetAppIdentity() throws Exception { - EnvironmentDetail envDetail = EnvironmentDetail.newBuilder().deviceName("device").appName("app").build(); + final EnvironmentDetail envDetail = + EnvironmentDetail.newBuilder().deviceName("device").appName("app").build(); - AppIdentity appIdentity = AppIdentity.newBuilder().deviceId(Integer.valueOf(123)).appNameId("456").build(); + final AppIdentity appIdentity = + AppIdentity.newBuilder().deviceId(123).appNameId("456").appName("app").build(); + + final ObjectMapper objectMapper = new ObjectMapper(); - ObjectMapper objectMapper = new ObjectMapper(); - String appIdentityResponse = objectMapper.writer().writeValueAsString(appIdentity); - - HttpClient httpClient = PowerMockito.mock(HttpClient.class); - PowerMockito.whenNew(HttpClient.class).withAnyArguments().thenReturn(httpClient); - PowerMockito.when(httpClient.post(Mockito.anyString(), (byte[]) Mockito.any())).thenReturn(appIdentityResponse); - - ApiConfiguration apiConfig = ApiConfiguration.newBuilder().apiUrl("url").apiKey("key").envDetail(envDetail).build(); - + + final HttpClient httpClient = mock(HttpClient.class); + whenNew(HttpClient.class).withAnyArguments().thenReturn(httpClient); + when(httpClient.post(Mockito.anyString(), (byte[]) Mockito.any())).thenReturn(appIdentityResponse); + + ApiConfiguration apiConfig = + ApiConfiguration.newBuilder().apiUrl("url").apiKey("key").application("app").envDetail(envDetail).build(); + AppIdentityService service = new AppIdentityService(apiConfig, objectMapper); Optional rv = service.getAppIdentity(); - + Assert.assertNotNull(rv); Assert.assertTrue(rv.isPresent()); Assert.assertEquals(Integer.valueOf(123), rv.get().getDeviceId());