/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 nl.info.flume.source; import com.google.common.base.Preconditions; import org.apache.commons.lang.StringUtils; import org.apache.flume.Context; import org.apache.flume.CounterGroup; import org.apache.flume.Event; import org.apache.flume.EventDrivenSource; import org.apache.flume.channel.ChannelProcessor; import org.apache.flume.conf.Configurable; import org.apache.flume.event.EventBuilder; import org.apache.flume.source.AbstractSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import static java.lang.String.format; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.CHARSET; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.CONFIG_BATCH_SIZE; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.CONFIG_LOG_STDERR; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.CONFIG_RESTART; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.CONFIG_RESTART_THROTTLE; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_BATCH_SIZE; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_CHARSET; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_LOG_STDERR; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_RESTART; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_RESTART_THROTTLE; import static nl.info.flume.source.MultiLineExecSourceConfigurationConstants.DEFAULT_LINE_TERMINATOR; /** * <p> * An implementation that executes a Unix process and turns each * group of lines of text, terminated by a certain line-terminator into an event. * It will */ public class MultiLineExecSource extends AbstractSource implements EventDrivenSource, Configurable { private static final Logger logger = LoggerFactory.getLogger(nl.info.flume.source.MultiLineExecSource.class); private String command; private String eventTerminator; private String lineTerminator; private CounterGroup counterGroup; private ExecutorService executor; private Future<?> runnerFuture; private long restartThrottle; private boolean restart; private boolean logStderr; private Integer bufferCount; private ExecRunnable runner; private Charset charset; @Override public void start() { logger.info("Multi Line Exec source starting with command: {}, event terminator: {}", command, eventTerminator); executor = Executors.newSingleThreadExecutor(); counterGroup = new CounterGroup(); runner = new ExecRunnable(command, eventTerminator, lineTerminator, getChannelProcessor(), counterGroup, restart, restartThrottle, logStderr, bufferCount, charset); // FIXME: Use a callback-like executor / future to signal us upon failure. runnerFuture = executor.submit(runner); /* * NB: This comes at the end rather than the beginning of the method because * it sets our state to running. We want to make sure the executor is alive * and well first. */ super.start(); logger.debug("Multi Line Exec source started"); } @Override public void stop() { logger.info("Stopping Multi Line exec source with command: {}, event terminator: {}", command, eventTerminator); if (runner != null) { runner.setRestart(false); runner.kill(); } if (runnerFuture != null) { logger.debug("Stopping Multi Line exec runner"); runnerFuture.cancel(true); logger.debug("Multi Line Exec runner stopped"); } executor.shutdown(); while (!executor.isTerminated()) { logger.debug("Waiting for Multi Line exec executor service to stop"); try { executor.awaitTermination(500, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { logger.debug("Interrupted while waiting for Multi Line exec executor service to stop. Just exiting."); Thread.currentThread().interrupt(); } } super.stop(); logger.debug(format("Multi Line Exec source with command: %s, event terminator: %s stopped. Metrics: %s", command, eventTerminator, counterGroup)); } @Override public void configure(Context context) { command = context.getString("command"); eventTerminator = context.getString("event.terminator"); lineTerminator = context.getString("line.terminator", DEFAULT_LINE_TERMINATOR); Preconditions.checkState(command != null, "The parameter command must be specified"); Preconditions.checkState(lineTerminator != null, "The parameter line.terminator must be specified"); restartThrottle = context.getLong(CONFIG_RESTART_THROTTLE, DEFAULT_RESTART_THROTTLE); restart = context.getBoolean(CONFIG_RESTART, DEFAULT_RESTART); logStderr = context.getBoolean(CONFIG_LOG_STDERR, DEFAULT_LOG_STDERR); bufferCount = context.getInteger(CONFIG_BATCH_SIZE, DEFAULT_BATCH_SIZE); charset = Charset.forName(context.getString(CHARSET, DEFAULT_CHARSET)); } protected static class ExecRunnable implements Runnable { public ExecRunnable(String command, String eventTerminator, String lineTerminator, ChannelProcessor channelProcessor, CounterGroup counterGroup, boolean restart, long restartThrottle, boolean logStderr, int bufferCount, Charset charset) { this.command = command; this.eventTerminator = eventTerminator; this.lineTerminator = lineTerminator; this.channelProcessor = channelProcessor; this.counterGroup = counterGroup; this.restartThrottle = restartThrottle; this.bufferCount = bufferCount; this.restart = restart; this.logStderr = logStderr; this.charset = charset; } private String command; private String eventTerminator; private String lineTerminator; private ChannelProcessor channelProcessor; private CounterGroup counterGroup; private volatile boolean restart; private long restartThrottle; private int bufferCount; private boolean logStderr; private Charset charset; private Process process = null; @Override public void run() { do { String exitCode; BufferedReader reader = null; try { process = startedCommandProcessBuilder(Arrays.asList(command.split("\\s+"))); reader = getBufferedReader(); // StderrLogger dies as soon as the input stream is invalid StderrReader stderrReader = getStderrReader(); stderrReader.setName("StderrReader-[" + command + "]"); stderrReader.setDaemon(true); stderrReader.start(); String line; boolean skipNextEmptyLine = false; List<Event> eventList = new ArrayList<Event>(); List<String> buffer = new ArrayList<String>(); while ((line = reader.readLine()) != null) { if (line.isEmpty() && skipNextEmptyLine) { skipNextEmptyLine = false; continue; } buffer.add(line); if (line.endsWith(eventTerminator)) { counterGroup.incrementAndGet("multi.line.exec.events.read"); String eventBody = StringUtils.join(buffer.toArray(), lineTerminator); buffer.clear(); eventList.add(EventBuilder.withBody(eventBody.getBytes(charset))); skipNextEmptyLine = true; } if (eventList.size() >= bufferCount) { channelProcessor.processEventBatch(eventList); eventList.clear(); } } if (!eventList.isEmpty()) { channelProcessor.processEventBatch(eventList); } } catch (Exception e) { logger.error("Failed while running command: " + command, e); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } } finally { if (reader != null) { try { reader.close(); } catch (IOException ex) { logger.error("Failed to close reader for Multi Line exec source", ex); } } exitCode = String.valueOf(kill()); } if (restart) { logger.info("Restarting in {}ms, exit code {}", restartThrottle, exitCode); try { Thread.sleep(restartThrottle); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } else { logger.info("Command [" + command + "] exited with " + exitCode); } } while (restart); } protected StderrReader getStderrReader() { return new StderrReader(new BufferedReader(new InputStreamReader(process.getErrorStream(), charset)), logStderr); } protected BufferedReader getBufferedReader() { return new BufferedReader(new InputStreamReader(process.getInputStream(), charset)); } protected Process startedCommandProcessBuilder(List<String> commandArgs) throws IOException { return new ProcessBuilder(commandArgs).start(); } public int kill() { if (process != null) { synchronized (process) { process.destroy(); try { return process.waitFor(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } return Integer.MIN_VALUE; } return Integer.MIN_VALUE / 2; } public void setRestart(boolean restart) { this.restart = restart; } } protected static class StderrReader extends Thread { private BufferedReader input; private boolean logStderr; protected StderrReader(BufferedReader input, boolean logStderr) { this.input = input; this.logStderr = logStderr; } @Override public void run() { try { int i = 0; String line; while ((line = input.readLine()) != null) { if (logStderr) { // There is no need to read 'line' with a charset // as we do not to propagate it. // It is in UTF-16 and would be printed in UTF-8 format. logger.info("StderrLogger[{}] = '{}'", ++i, line); } } } catch (IOException e) { logger.info("StderrLogger exiting", e); } finally { try { if (input != null) { input.close(); } } catch (IOException ex) { logger.error("Failed to close stderr reader for Multi Line exec source", ex); } } } } }