/*
* Copyright 2013-2015 Rackspace
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rackspacecloud.blueflood.inputs.handlers;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.rackspacecloud.blueflood.cache.ConfigTtlProvider;
import com.rackspacecloud.blueflood.exceptions.InvalidDataException;
import com.rackspacecloud.blueflood.http.DefaultHandler;
import com.rackspacecloud.blueflood.http.HttpRequestHandler;
import com.rackspacecloud.blueflood.inputs.formats.JSONMetric;
import com.rackspacecloud.blueflood.inputs.formats.JSONMetricsContainer;
import com.rackspacecloud.blueflood.io.Constants;
import com.rackspacecloud.blueflood.io.Instrumentation;
import com.rackspacecloud.blueflood.outputs.formats.ErrorResponse;
import com.rackspacecloud.blueflood.tracker.Tracker;
import com.rackspacecloud.blueflood.types.IMetric;
import com.rackspacecloud.blueflood.types.Metric;
import com.rackspacecloud.blueflood.types.MetricsCollection;
import com.rackspacecloud.blueflood.utils.Metrics;
import com.rackspacecloud.blueflood.utils.TimeValue;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.TypeFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
public class HttpMetricsIngestionHandler implements HttpRequestHandler {
public static final String ERROR_HEADER = "The following errors have been encountered:";
private static final Logger log = LoggerFactory.getLogger(HttpMetricsIngestionHandler.class);
private static final Counter requestCount = Metrics.counter(HttpMetricsIngestionHandler.class, "HTTP Request Count");
private static final Meter requestsReceived = Metrics.meter(HttpMetricsIngestionHandler.class, "Http Requests received");
protected final ObjectMapper mapper;
protected final TypeFactory typeFactory;
private final HttpMetricsIngestionServer.Processor processor;
private final TimeValue timeout;
protected boolean enablePerTenantMetrics;
private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
protected static final Validator validator = factory.getValidator();
// Metrics
private static final Timer jsonTimer = Metrics.timer(HttpMetricsIngestionHandler.class, "HTTP Ingestion json processing timer");
private static final Timer persistingTimer = Metrics.timer(HttpMetricsIngestionHandler.class, "HTTP Ingestion persisting timer");
public static String getResponseBody( List<String> errors ) {
StringBuilder sb = new StringBuilder();
sb.append( ERROR_HEADER + System.lineSeparator() );
for( String error : errors ) {
sb.append( error + System.lineSeparator() );
}
return sb.toString();
}
public HttpMetricsIngestionHandler(HttpMetricsIngestionServer.Processor processor, TimeValue timeout) {
this(processor, timeout, false);
}
public HttpMetricsIngestionHandler(HttpMetricsIngestionServer.Processor processor, TimeValue timeout, boolean enablePerTenantMetrics) {
this.mapper = new ObjectMapper();
this.typeFactory = TypeFactory.defaultInstance();
this.timeout = timeout;
this.processor = processor;
this.enablePerTenantMetrics = enablePerTenantMetrics;
}
protected JSONMetricsContainer createContainer(String body, String tenantId) throws JsonParseException, JsonMappingException, IOException {
//mapping
List<JSONMetric> jsonMetrics =
mapper.readValue(
body,
typeFactory.constructCollectionType(List.class, JSONMetric.class)
);
//validation
List<ErrorResponse.ErrorData> validationErrors = new ArrayList<ErrorResponse.ErrorData>();
List<JSONMetric> validJsonMetrics = new ArrayList<JSONMetric>();
for (JSONMetric metric: jsonMetrics) {
Set<ConstraintViolation<JSONMetric>> constraintViolations = validator.validate(metric);
if (constraintViolations.size() == 0) {
validJsonMetrics.add(metric);
} else {
for (ConstraintViolation<JSONMetric> constraintViolation : constraintViolations) {
validationErrors.add(new ErrorResponse.ErrorData(tenantId, metric.getMetricName(),
constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage(),
metric.getCollectionTime()));
}
}
}
return new JSONMetricsContainer(tenantId, validJsonMetrics, validationErrors);
}
@Override
public void handle(ChannelHandlerContext ctx, FullHttpRequest request) {
try {
requestsReceived.mark();
Tracker.getInstance().track(request);
requestCount.inc();
final String tenantId = request.headers().get(HttpMetricsIngestionServer.TENANT_ID_HEADER);
JSONMetricsContainer jsonMetricsContainer;
List<Metric> validMetrics;
final Timer.Context jsonTimerContext = jsonTimer.time();
final String body = request.content().toString(Constants.DEFAULT_CHARSET);
try {
jsonMetricsContainer = createContainer(body, tenantId);
if (jsonMetricsContainer.areDelayedMetricsPresent()) {
Tracker.getInstance().trackDelayedMetricsTenant(tenantId, jsonMetricsContainer.getDelayedMetrics());
}
validMetrics = jsonMetricsContainer.getValidMetrics();
forceTTLsIfConfigured(validMetrics);
} catch (JsonParseException e) {
log.warn("Exception parsing content", e);
DefaultHandler.sendErrorResponse(ctx, request, "Cannot parse content",
HttpResponseStatus.BAD_REQUEST);
return;
} catch (JsonMappingException e) {
log.warn("Exception parsing content", e);
DefaultHandler.sendErrorResponse(ctx, request, "Cannot parse content",
HttpResponseStatus.BAD_REQUEST);
return;
} catch (InvalidDataException ex) {
// todo: we should measure these. if they spike, we track down the bad client.
// this is strictly a client problem. Something wasn't right (data out of range, etc.)
log.warn(ctx.channel().remoteAddress() + " " + ex.getMessage());
DefaultHandler.sendErrorResponse(ctx, request, "Invalid data " + ex.getMessage(),
HttpResponseStatus.BAD_REQUEST);
return;
} catch (IOException e) {
log.warn("IO Exception parsing content", e);
DefaultHandler.sendErrorResponse(ctx, request, "Cannot parse content",
HttpResponseStatus.BAD_REQUEST);
return;
} catch (Exception e) {
log.warn("Other exception while trying to parse content", e);
DefaultHandler.sendErrorResponse(ctx, request, "Failed parsing content",
HttpResponseStatus.INTERNAL_SERVER_ERROR);
return;
} finally {
jsonTimerContext.stop();
}
List<ErrorResponse.ErrorData> validationErrors = jsonMetricsContainer.getValidationErrors();
// If no valid metrics are present, return error response
if (validMetrics.isEmpty()) {
log.warn(ctx.channel().remoteAddress() + " No valid metrics");
if (validationErrors.isEmpty()) {
DefaultHandler.sendErrorResponse(ctx, request, "No valid metrics",
HttpResponseStatus.BAD_REQUEST);
} else {
DefaultHandler.sendErrorResponse(ctx, request, validationErrors, HttpResponseStatus.BAD_REQUEST);
}
return;
}
final MetricsCollection collection = new MetricsCollection();
collection.add(new ArrayList<IMetric>(validMetrics));
final Timer.Context persistingTimerContext = persistingTimer.time();
try {
ListenableFuture<List<Boolean>> futures = processor.apply(collection);
List<Boolean> persisteds = futures.get(timeout.getValue(), timeout.getUnit());
for (Boolean persisted : persisteds) {
if (!persisted) {
log.warn("Trouble persisting metrics:");
log.warn(String.format("%s", Arrays.toString(validMetrics.toArray())));
DefaultHandler.sendErrorResponse(ctx, request, "Persisted failed for metrics",
HttpResponseStatus.INTERNAL_SERVER_ERROR);
return;
}
}
recordPerTenantMetrics(tenantId, jsonMetricsContainer.getNonDelayedMetricsCount(),
jsonMetricsContainer.getDelayedMetricsCount());
// after processing metrics, return either OK or MULTI_STATUS depending on number of valid metrics
if( !validationErrors.isEmpty() ) {
// has some validation errors, return MULTI_STATUS
DefaultHandler.sendErrorResponse(ctx, request, validationErrors, HttpResponseStatus.MULTI_STATUS);
}
else {
// no validation error, return OK
DefaultHandler.sendResponse(ctx, request, null, HttpResponseStatus.OK);
}
} catch (TimeoutException e) {
DefaultHandler.sendErrorResponse(ctx, request, "Timed out persisting metrics", HttpResponseStatus.ACCEPTED);
} catch (Exception e) {
log.error("Exception persisting metrics", e);
DefaultHandler.sendErrorResponse(ctx, request, "Error persisting metrics", HttpResponseStatus.INTERNAL_SERVER_ERROR);
} finally {
persistingTimerContext.stop();
}
} finally {
requestCount.dec();
}
}
@VisibleForTesting
void recordPerTenantMetrics(String tenantId, int metricsCount, int delayedMetricsCount) {
if ( enablePerTenantMetrics ) {
Instrumentation.getIngestedMetricsMeter(tenantId).mark(metricsCount);
Instrumentation.getIngestedDelayedMetricsMeter(tenantId).mark(delayedMetricsCount);
}
}
private void forceTTLsIfConfigured(List<Metric> containerMetrics) {
ConfigTtlProvider configTtlProvider = ConfigTtlProvider.getInstance();
if(configTtlProvider.areTTLsForced()) {
for(Metric m : containerMetrics) {
m.setTtl(configTtlProvider.getConfigTTLForIngestion());
}
}
}
}