io.fabric8.maven.docker.access.log.LogRequestor.java Source code

Java tutorial

Introduction

Here is the source code for io.fabric8.maven.docker.access.log.LogRequestor.java

Source

package io.fabric8.maven.docker.access.log;/*
                                           *
                                           * Copyright 2014 Roland Huss
                                           *
                                           * 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.
                                           */

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import io.fabric8.maven.docker.access.DockerAccessException;
import io.fabric8.maven.docker.access.UrlBuilder;
import io.fabric8.maven.docker.access.util.RequestUtil;
import io.fabric8.maven.docker.util.Timestamp;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;

/**
 * Extractor for parsing the response of a log request
 *
 * @author roland
 * @since 28/11/14
 */
public class LogRequestor extends Thread implements LogGetHandle {

    // Patter for matching log entries
    static final Pattern LOG_LINE = Pattern.compile("^\\[?(?<timestamp>[^\\s\\]]*)]?\\s+(?<entry>.*?)\\s*$",
            Pattern.DOTALL);
    private final CloseableHttpClient client;

    private final String containerId;

    // callback called for each line extracted
    private LogCallback callback;

    private DockerAccessException exception;

    // Remember for asynchronous handling so that the request can be aborted from the outside
    private HttpUriRequest request;

    private final UrlBuilder urlBuilder;

    // Lock for synchronizing closing of requests
    private final Object lock = new Object();

    /**
     * Create a helper object for requesting log entries synchronously ({@link #fetchLogs()}) or asynchronously ({@link #start()}.
     *
     * @param client HTTP client to use for requesting the docker host
     * @param urlBuilder builder that creates docker urls
     * @param containerId container for which to fetch the host
     * @param callback callback to call for each line received
     */
    public LogRequestor(CloseableHttpClient client, UrlBuilder urlBuilder, String containerId,
            LogCallback callback) {
        this.client = client;
        this.containerId = containerId;

        this.urlBuilder = urlBuilder;

        this.callback = callback;
        this.exception = null;
        this.setDaemon(true);
    }

    /**
     * Get logs and feed a callback with the content
     */
    public void fetchLogs() {
        try {
            HttpResponse resp = client.execute(getLogRequest(false));
            parseResponse(resp);
        } catch (IOException exp) {
            callback.error(exp.getMessage());
        }
    }

    // Fetch log asynchronously as stream and follow stream
    public void run() {
        // Response to extract from

        try {
            request = getLogRequest(true);
            HttpResponse response = client.execute(request);
            parseResponse(response);
        } catch (IOException exp) {
            callback.error("IO Error while requesting logs: " + exp);
        } finally {
            try {
                synchronized (lock) {
                    client.close();
                    request = null;
                }
            } catch (IOException exp) {
                callback.error("Error while closing client: " + exp);
            }
        }
    }

    private static class NoBytesReadException extends IOException {
        public NoBytesReadException() {
        }
    }

    /**
     * This is a copy of ByteStreams.readFully(), with the slight change that it throws
     * NoBytesReadException if zero bytes are read. Otherwise it is identical.
     *
     * @param in
     * @param bytes
     * @throws IOException
     */
    private void readFully(InputStream in, byte[] bytes) throws IOException {
        int read = ByteStreams.read(in, bytes, 0, bytes.length);
        if (read == 0) {
            throw new NoBytesReadException();
        } else if (read != bytes.length) {
            throw new EOFException(
                    "reached end of stream after reading " + read + " bytes; " + bytes.length + " bytes expected");
        }
    }

    private boolean readStreamFrame(InputStream is) throws IOException, LogCallback.DoneException {
        // Read the header, which is composed of eight bytes. The first byte is an integer
        // indicating the stream type (0 = stdin, 1 = stdout, 2 = stderr), the next three are thrown
        // out, and the final four are the size of the remaining stream as an integer.
        ByteBuffer headerBuffer = ByteBuffer.allocate(8);
        headerBuffer.order(ByteOrder.BIG_ENDIAN);
        try {
            this.readFully(is, headerBuffer.array());
        } catch (NoBytesReadException e) {
            // Not bytes read for stream. Return false to stop consuming stream.
            return false;
        } catch (EOFException e) {
            throw new IOException("Failed to read log header. Could not read all 8 bytes. " + e.getMessage(), e);
        }

        // Grab the stream type (stdout, stderr, stdin) from first byte and throw away other 3 bytes.
        int type = headerBuffer.get();

        // Skip three bytes, then read size from remaining four bytes.
        int size = headerBuffer.getInt(4);

        // Ignore empty messages and keep reading.
        if (size <= 0) {
            return true;
        }

        // Read the actual message
        ByteBuffer payload = ByteBuffer.allocate(size);
        try {
            ByteStreams.readFully(is, payload.array());
        } catch (EOFException e) {
            throw new IOException("Failed to read log message. Could not read all " + size + " bytes. "
                    + e.getMessage() + " [ Header: " + Hex.encodeHexString(headerBuffer.array()) + "]", e);
        }

        String message = Charsets.UTF_8.newDecoder().decode(payload).toString();
        callLogCallback(type, message);
        return true;
    }

    private void parseResponse(HttpResponse response) {
        StatusLine status = response.getStatusLine();
        if (status.getStatusCode() != 200) {
            exception = new DockerAccessException("Error while reading logs (" + status + ")");
        }
        try (InputStream is = response.getEntity().getContent()) {
            while (true) {
                if (!readStreamFrame(is)) {
                    return;
                }
            }
        } catch (IOException e) {
            callback.error("Cannot process chunk response: " + e);
            finish();
        } catch (LogCallback.DoneException e) {
            // Can be thrown by a log callback which indicates that we are done.
            finish();
        }
    }

    private void callLogCallback(int type, String txt) throws LogCallback.DoneException {
        Matcher matcher = LOG_LINE.matcher(txt);
        if (!matcher.matches()) {
            callback.error(
                    String.format("Invalid log format for '%s' (expected: \"<timestamp> <txt>\") [%04x %04x]", txt,
                            (int) (txt.toCharArray())[0], (int) (txt.toCharArray())[1]));
            throw new LogCallback.DoneException();
        }
        Timestamp ts = new Timestamp(matcher.group("timestamp"));
        String logTxt = matcher.group("entry");
        callback.log(type, ts, logTxt);
    }

    private HttpUriRequest getLogRequest(boolean follow) {
        return RequestUtil.newGet(urlBuilder.containerLogs(containerId, follow));
    }

    @Override
    public void finish() {
        if (request != null) {
            synchronized (lock) {
                if (request != null) {
                    request.abort();
                }
            }
        }
    }

    @Override
    public boolean isError() {
        return exception != null;
    }

    @Override
    public DockerAccessException getException() {
        return exception;
    }
}