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