/** * Copyright 2015 Netflix, Inc. * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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.netflix.servo.publish.atlas; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.netflix.servo.util.Throwables; import iep.com.netflix.iep.http.RxHttp; import iep.io.reactivex.netty.protocol.http.client.HttpClientRequest; import iep.io.reactivex.netty.protocol.http.client.HttpClientResponse; import iep.io.reactivex.netty.protocol.http.client.HttpResponseHeaders; import io.netty.buffer.ByteBuf; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.Subscription; import rx.exceptions.CompositeException; import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.Schedulers; import javax.inject.Inject; import javax.inject.Singleton; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * Helper class to make http requests using rxhttp. For internal use of servo only. */ @Singleton public final class HttpHelper { private static final JsonFactory SMILE_FACTORY = new SmileFactory(); private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); private static final String SMILE_CONTENT_TYPE = "application/x-jackson-smile"; private final RxHttp rxHttp; /** * An HTTP Response. For internal use of servo only. */ public static class Response { private int status; private byte[] body; private HttpResponseHeaders headers; /** * Get the HTTP status code. */ public int getStatus() { return status; } /** * Get the body of the response as a byte array. */ public byte[] getBody() { return Arrays.copyOf(body, body.length); } /** * Get the rxnetty {@link HttpResponseHeaders} for this response. */ public HttpResponseHeaders getHeaders() { return headers; } } /** * Create a new HttpHelper using the given {@link RxHttp} instance. */ @Inject public HttpHelper(RxHttp rxHttp) { this.rxHttp = rxHttp; } /** * Get the underlying {@link RxHttp} instance. */ public RxHttp getRxHttp() { return rxHttp; } /** * POST to the given URI the passed {@link JsonPayload}. */ public Observable<HttpClientResponse<ByteBuf>> postSmile(String uriStr, JsonPayload payload) { byte[] entity = toByteArray(SMILE_FACTORY, payload); URI uri = URI.create(uriStr); return rxHttp.post(uri, SMILE_CONTENT_TYPE, entity); } private byte[] toByteArray(JsonFactory factory, JsonPayload payload) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonGenerator gen = factory.createGenerator(baos, JsonEncoding.UTF8); payload.toJson(gen); gen.close(); baos.close(); return baos.toByteArray(); } catch (IOException e) { throw Throwables.propagate(e); } } private void logErr(String prefix, Throwable e, int sent, int total) { if (LOGGER.isWarnEnabled()) { final Throwable cause = e.getCause() != null ? e.getCause() : e; String msg = String.format("%s exception %s:%s Sent %d/%d", prefix, cause.getClass().getSimpleName(), cause.getMessage(), sent, total); LOGGER.warn(msg); if (cause instanceof CompositeException) { CompositeException ce = (CompositeException) cause; for (Throwable t : ce.getExceptions()) { LOGGER.warn(" Exception {}: {}", t.getClass().getSimpleName(), t.getMessage()); } } } } /** * Attempt to send all the batches totalling numMetrics in the allowed time. * * @return The total number of metrics sent. */ public int sendAll(Iterable<Observable<Integer>> batches, final int numMetrics, long timeoutMillis) { final AtomicBoolean err = new AtomicBoolean(false); final AtomicInteger updated = new AtomicInteger(0); LOGGER.debug("Got {} ms to send {} metrics", timeoutMillis, numMetrics); try { final CountDownLatch completed = new CountDownLatch(1); final Subscription s = Observable.mergeDelayError(Observable.from(batches)) .timeout(timeoutMillis, TimeUnit.MILLISECONDS) .subscribeOn(Schedulers.immediate()) .subscribe(updated::addAndGet, exc -> { logErr("onError caught", exc, updated.get(), numMetrics); err.set(true); completed.countDown(); }, completed::countDown); try { completed.await(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException interrupted) { err.set(true); s.unsubscribe(); LOGGER.warn("Timed out sending metrics. {}/{} sent", updated.get(), numMetrics); } } catch (Exception e) { err.set(true); logErr("Unexpected ", e, updated.get(), numMetrics); } if (updated.get() < numMetrics && !err.get()) { LOGGER.warn("No error caught, but only {}/{} sent.", updated.get(), numMetrics); } return updated.get(); } /** * Perform an HTTP get in the allowed time. */ public Response get(HttpClientRequest<ByteBuf> req, long timeout, TimeUnit timeUnit) { final String uri = req.getUri(); final Response result = new Response(); try { final Func1<HttpClientResponse<ByteBuf>, Observable<byte[]>> process = response -> { result.status = response.getStatus().code(); result.headers = response.getHeaders(); final Func2<ByteArrayOutputStream, ByteBuf, ByteArrayOutputStream> accumulator = (baos, bb) -> { try { bb.readBytes(baos, bb.readableBytes()); } catch (IOException e) { throw new RuntimeException(e); } return baos; }; return response.getContent() .reduce(new ByteArrayOutputStream(), accumulator) .map(ByteArrayOutputStream::toByteArray); }; result.body = rxHttp.submit(req) .flatMap(process) .subscribeOn(Schedulers.io()) .toBlocking() .toFuture() .get(timeout, timeUnit); return result; } catch (Exception e) { throw new RuntimeException("failed to get url: " + uri, e); } } }