001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.flume.appender;
018
019import java.util.HashMap;
020import java.util.Locale;
021import java.util.Map;
022import java.util.concurrent.TimeUnit;
023
024import org.apache.flume.Event;
025import org.apache.flume.EventDeliveryException;
026import org.apache.flume.agent.embedded.EmbeddedAgent;
027import org.apache.logging.log4j.LoggingException;
028import org.apache.logging.log4j.core.appender.ManagerFactory;
029import org.apache.logging.log4j.core.config.ConfigurationException;
030import org.apache.logging.log4j.core.config.Property;
031import org.apache.logging.log4j.core.util.NameUtil;
032import org.apache.logging.log4j.util.PropertiesUtil;
033import org.apache.logging.log4j.util.Strings;
034
035/**
036 *
037 */
038public class FlumeEmbeddedManager extends AbstractFlumeManager {
039
040    private static final String FILE_SEP = PropertiesUtil.getProperties().getStringProperty("file.separator");
041
042    private static final String IN_MEMORY = "InMemory";
043
044    private static FlumeManagerFactory factory = new FlumeManagerFactory();
045
046    private final EmbeddedAgent agent;
047
048    private final String shortName;
049
050
051    /**
052     * Constructor
053     * @param name The unique name of this manager.
054     * @param shortName The short version of the agent name.
055     * @param agent The embedded agent.
056     */
057    protected FlumeEmbeddedManager(final String name, final String shortName, final EmbeddedAgent agent) {
058        super(name);
059        this.agent = agent;
060        this.shortName = shortName;
061    }
062
063    /**
064     * Returns a FlumeEmbeddedManager.
065     * @param name The name of the manager.
066     * @param agents The agents to use.
067     * @param properties Properties for the embedded manager.
068     * @param batchSize The number of events to include in a batch.
069     * @param dataDir The directory where the Flume FileChannel should write to.
070     * @return A FlumeAvroManager.
071     */
072    public static FlumeEmbeddedManager getManager(final String name, final Agent[] agents, final Property[] properties,
073                                                  int batchSize, final String dataDir) {
074
075        if (batchSize <= 0) {
076            batchSize = 1;
077        }
078
079        if ((agents == null || agents.length == 0) && (properties == null || properties.length == 0)) {
080            throw new IllegalArgumentException("Either an Agent or properties are required");
081        } else if (agents != null && agents.length > 0 && properties != null && properties.length > 0) {
082            throw new IllegalArgumentException("Cannot configure both Agents and Properties.");
083        }
084
085        final StringBuilder sb = new StringBuilder();
086        boolean first = true;
087
088        if (agents != null && agents.length > 0) {
089            sb.append(name).append('[');
090            for (final Agent agent : agents) {
091                if (!first) {
092                    sb.append('_');
093                }
094                sb.append(agent.getHost()).append('-').append(agent.getPort());
095                first = false;
096            }
097            sb.append(']');
098        } else {
099            String sep = Strings.EMPTY;
100            sb.append(name).append('-');
101            final StringBuilder props = new StringBuilder();
102            for (final Property prop : properties) {
103                props.append(sep);
104                props.append(prop.getName()).append('=').append(prop.getValue());
105                sep = "_";
106            }
107            sb.append(NameUtil.md5(props.toString()));
108        }
109        return getManager(sb.toString(), factory,
110                new FactoryData(name, agents, properties, batchSize, dataDir));
111    }
112
113    @Override
114    public void send(final Event event) {
115        try {
116            agent.put(event);
117        } catch (final EventDeliveryException ex) {
118            throw new LoggingException("Unable to deliver event to Flume Appender " + shortName, ex);
119        }
120    }
121
122    @Override
123    protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {
124        agent.stop();
125        return true;
126    }
127
128    /**
129     * Factory data.
130     */
131    private static class FactoryData {
132        private final Agent[] agents;
133        private final Property[] properties;
134        private final int batchSize;
135        private final String dataDir;
136        private final String name;
137
138        /**
139         * Constructor.
140         * @param name The name of the Appender.
141         * @param agents The agents.
142         * @param properties The Flume configuration properties.
143         * @param batchSize The number of events to include in a batch.
144         * @param dataDir The directory where Flume should write to.
145         */
146        public FactoryData(final String name, final Agent[] agents, final Property[] properties, final int batchSize,
147                           final String dataDir) {
148            this.name = name;
149            this.agents = agents;
150            this.batchSize = batchSize;
151            this.properties = properties;
152            this.dataDir = dataDir;
153        }
154    }
155
156    /**
157     * Avro Manager Factory.
158     */
159    private static class FlumeManagerFactory implements ManagerFactory<FlumeEmbeddedManager, FactoryData> {
160
161        /**
162         * Create the FlumeAvroManager.
163         * @param name The name of the entity to manage.
164         * @param data The data required to create the entity.
165         * @return The FlumeAvroManager.
166         */
167        @Override
168        public FlumeEmbeddedManager createManager(final String name, final FactoryData data) {
169            try {
170                final Map<String, String> props = createProperties(data.name, data.agents, data.properties,
171                    data.batchSize, data.dataDir);
172                final EmbeddedAgent agent = new EmbeddedAgent(name);
173                agent.configure(props);
174                agent.start();
175                LOGGER.debug("Created Agent " + name);
176                return new FlumeEmbeddedManager(name, data.name, agent);
177            } catch (final Exception ex) {
178                LOGGER.error("Could not create FlumeEmbeddedManager", ex);
179            }
180            return null;
181        }
182
183        private Map<String, String> createProperties(final String name, final Agent[] agents,
184                                                     final Property[] properties, final int batchSize, String dataDir) {
185            final Map<String, String> props = new HashMap<>();
186
187            if ((agents == null || agents.length == 0) && (properties == null || properties.length == 0)) {
188                LOGGER.error("No Flume configuration provided");
189                throw new ConfigurationException("No Flume configuration provided");
190            }
191
192            if (agents != null && agents.length > 0 && properties != null && properties.length > 0) {
193                LOGGER.error("Agents and Flume configuration cannot both be specified");
194                throw new ConfigurationException("Agents and Flume configuration cannot both be specified");
195            }
196
197            if (agents != null && agents.length > 0) {
198
199                if (Strings.isNotEmpty(dataDir)) {
200                    if (dataDir.equals(IN_MEMORY)) {
201                        props.put("channel.type", "memory");
202                    } else {
203                        props.put("channel.type", "file");
204
205                        if (!dataDir.endsWith(FILE_SEP)) {
206                            dataDir = dataDir + FILE_SEP;
207                        }
208
209                        props.put("channel.checkpointDir", dataDir + "checkpoint");
210                        props.put("channel.dataDirs", dataDir + "data");
211                    }
212
213                } else {
214                    props.put("channel.type", "file");
215                }
216
217                final StringBuilder sb = new StringBuilder();
218                String leading = Strings.EMPTY;
219                final int priority = agents.length;
220                for (int i = 0; i < priority; ++i) {
221                    sb.append(leading).append("agent").append(i);
222                    leading = " ";
223                    final String prefix = "agent" + i;
224                    props.put(prefix + ".type", "avro");
225                    props.put(prefix + ".hostname", agents[i].getHost());
226                    props.put(prefix + ".port", Integer.toString(agents[i].getPort()));
227                    props.put(prefix + ".batch-size", Integer.toString(batchSize));
228                    props.put("processor.priority." + prefix, Integer.toString(agents.length - i));
229                }
230                props.put("sinks", sb.toString());
231                props.put("processor.type", "failover");
232            } else {
233                String[] sinks = null;
234
235                for (final Property property : properties) {
236                    final String key = property.getName();
237
238                    if (Strings.isEmpty(key)) {
239                        final String msg = "A property name must be provided";
240                        LOGGER.error(msg);
241                        throw new ConfigurationException(msg);
242                    }
243
244                    final String upperKey = key.toUpperCase(Locale.ENGLISH);
245
246                    if (upperKey.startsWith(name.toUpperCase(Locale.ENGLISH))) {
247                        final String msg =
248                            "Specification of the agent name is not allowed in Flume Appender configuration: " + key;
249                        LOGGER.error(msg);
250                        throw new ConfigurationException(msg);
251                    }
252
253                    final String value = property.getValue();
254                    if (Strings.isEmpty(value)) {
255                        final String msg = "A value for property " + key + " must be provided";
256                        LOGGER.error(msg);
257                        throw new ConfigurationException(msg);
258                    }
259
260                    if (upperKey.equals("SINKS")) {
261                        sinks = value.trim().split(" ");
262                    }
263
264                    props.put(key, value);
265                }
266
267                if (sinks == null || sinks.length == 0) {
268                    final String msg = "At least one Sink must be specified";
269                    LOGGER.error(msg);
270                    throw new ConfigurationException(msg);
271                }
272            }
273            return props;
274        }
275
276    }
277
278}