All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.fabric8.maven.docker.access.log.LogRequestor Maven / Gradle / Ivy

There is a newer version: 0.45.0
Show newest version
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.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("^\\[?(?[^\\s\\]]*)]? (?.*?)\\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;

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

    /**
     * Get logs and feed a callback with the content
     */
    public void fetchLogs() {
        try {
            callback.open();
            this.request = getLogRequest(false);
            final HttpResponse response = client.execute(request);
            parseResponse(response);
        } catch (LogCallback.DoneException e) {
            // Signifies we're finished with the log stream.
        } catch (IOException exp) {
            callback.error(exp.getMessage());
        } finally {
            callback.close();
        }
    }

    // Fetch log asynchronously as stream and follow stream
    public void run() {
        try {
            callback.open();
            this.request = getLogRequest(true);
            final HttpResponse response = client.execute(request);
            parseResponse(response);
        } catch (LogCallback.DoneException e) {
            // Signifies we're finished with the log stream.
        } catch (IOException e) {
            callback.error("IO Error while requesting logs: " + e + " " + Thread.currentThread().getName());
        } finally {
            callback.close();
        }
    }

    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) throws LogCallback.DoneException, IOException {
        final StatusLine status = response.getStatusLine();
        if (status.getStatusCode() != 200) {
            exception = new DockerAccessException("Error while reading logs (" + status + ")");
            throw new LogCallback.DoneException();
        }

        try (InputStream is = response.getEntity().getContent()) {
            while (true) {
                if (!readStreamFrame(is)) {
                    return;
                }
            }
        }
    }

    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: \" \") [%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) {
            request.abort();
            request = null;
        }
    }

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

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy