This is an automated email from the ASF dual-hosted git repository.
rgoers pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-flume-rpc.git
The following commit(s) were added to refs/heads/main by this push:
new c80723b Move embedded agent to this project
c80723b is described below
commit c80723b51ea7f8f6707adc530465add21e546466
Author: Ralph Goers <[email protected]>
AuthorDate: Sat May 30 12:50:45 2026 -0700
Move embedded agent to this project
---
.github/workflows/build.yml | 4 +-
flume-embedded-agent/pom.xml | 98 +++++++
.../apache/flume/agent/embedded/EmbeddedAgent.java | 259 +++++++++++++++++
.../agent/embedded/EmbeddedAgentConfiguration.java | 323 +++++++++++++++++++++
.../flume/agent/embedded/EmbeddedSource.java | 52 ++++
.../MaterializedConfigurationProvider.java | 42 +++
.../embedded/MemoryConfigurationProvider.java | 48 +++
.../apache/flume/agent/embedded/package-info.java | 24 ++
.../flume/agent/embedded/TestEmbeddedAgent.java | 230 +++++++++++++++
.../embedded/TestEmbeddedAgentConfiguration.java | 151 ++++++++++
.../embedded/TestEmbeddedAgentEmbeddedSource.java | 209 +++++++++++++
.../agent/embedded/TestEmbeddedAgentState.java | 125 ++++++++
flume-embedded-agent/src/test/resources/log4j2.xml | 31 ++
pom.xml | 1 +
14 files changed, 1595 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e786b72..284bc1a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -52,11 +52,11 @@ jobs:
uses: actions/checkout@v6
# JDK 8 is needed for the build, and it is the primary bytecode target.
- - name: Setup JDK 8
+ - name: Setup JDK 17
uses: actions/setup-java@v5
with:
distribution: ${{ matrix.java-distribution }}
- java-version: 8
+ java-version: 17
cache: maven
- name: Build with Maven
diff --git a/flume-embedded-agent/pom.xml b/flume-embedded-agent/pom.xml
new file mode 100644
index 0000000..42c9ac3
--- /dev/null
+++ b/flume-embedded-agent/pom.xml
@@ -0,0 +1,98 @@
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <artifactId>flume-rpc</artifactId>
+ <groupId>org.apache.flume</groupId>
+ <version>2.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>flume-embedded-agent</artifactId>
+ <name>Flume Embedded Agent</name>
+ <description>Flume Embedded Agent: Stable public API for Embedding a Flume
2.x Agent</description>
+
+ <properties>
+ <!-- TODO fix pmd violations -->
+ <pmd.maxAllowedViolations>2</pmd.maxAllowedViolations>
+ <module.name>org.apache.flume.agent.embedded</module.name>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.apache.flume</groupId>
+ <artifactId>flume-ng-sdk</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flume</groupId>
+ <artifactId>flume-ng-configuration</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flume</groupId>
+ <artifactId>flume-ng-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flume</groupId>
+ <artifactId>flume-ng-node</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flume.flume-ng-channels</groupId>
+ <artifactId>flume-file-channel</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flume</groupId>
+ <artifactId>flume-rpc-avro</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <!-- only used for tests -->
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-1.2-api</artifactId>
+ <!-- only used for tests -->
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgent.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgent.java
new file mode 100644
index 0000000..45c9e43
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgent.java
@@ -0,0 +1,259 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+import org.apache.flume.Channel;
+import org.apache.flume.exception.ChannelException;
+import org.apache.flume.Event;
+import org.apache.flume.EventDeliveryException;
+import org.apache.flume.FlumeException;
+import org.apache.flume.SinkRunner;
+import org.apache.flume.Source;
+import org.apache.flume.SourceRunner;
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.conf.LogPrivacyUtil;
+import org.apache.flume.lifecycle.LifecycleAware;
+import org.apache.flume.lifecycle.LifecycleState;
+import org.apache.flume.lifecycle.LifecycleSupervisor;
+import org.apache.flume.lifecycle.LifecycleSupervisor.SupervisorPolicy;
+import org.apache.flume.node.MaterializedConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+/**
+ * EmbeddedAgent gives Flume users the ability to embed simple agents in
+ * applications. This Agent is mean to be much simpler than a traditional
+ * agent and as such it's more restrictive than what can be configured
+ * for a traditional agent. For specifics see the Flume Developer Guide.
+ */
[email protected]
[email protected]
+public class EmbeddedAgent {
+ private static final Logger LOGGER = LoggerFactory
+ .getLogger(EmbeddedAgent.class);
+
+ private final MaterializedConfigurationProvider configurationProvider;
+ private final String name;
+
+ private final LifecycleSupervisor supervisor;
+ private State state;
+ private SourceRunner sourceRunner;
+ private Channel channel;
+ private SinkRunner sinkRunner;
+ private EmbeddedSource embeddedSource;
+
+ @InterfaceAudience.Private
+ @InterfaceStability.Unstable
+ @VisibleForTesting
+ EmbeddedAgent(MaterializedConfigurationProvider configurationProvider,
+ String name) {
+ this.configurationProvider = configurationProvider;
+ this.name = name;
+ state = State.NEW;
+ supervisor = new LifecycleSupervisor();
+
+ }
+
+ public EmbeddedAgent(String name) {
+ this(new MaterializedConfigurationProvider(), name);
+ }
+
+ /**
+ * Configures the embedded agent. Can only be called after the object
+ * is created or after the stop() method is called.
+ *
+ * @param properties source, channel, and sink group configuration
+ * @throws FlumeException if a component is unable to be found or configured
+ * @throws IllegalStateException if called while the agent is started
+ */
+ public void configure(Map<String, String> properties)
+ throws FlumeException {
+ if (state == State.STARTED) {
+ throw new IllegalStateException("Cannot be configured while started");
+ }
+ doConfigure(properties);
+ state = State.STOPPED;
+ }
+
+ /**
+ * Started the agent. Can only be called after a successful call to
+ * configure().
+ *
+ * @throws FlumeException if a component cannot be started
+ * @throws IllegalStateException if the agent has not been configured or is
+ * already started
+ */
+ public void start()
+ throws FlumeException {
+ if (state == State.STARTED) {
+ throw new IllegalStateException("Cannot be started while started");
+ } else if (state == State.NEW) {
+ throw new IllegalStateException("Cannot be started before being " +
+ "configured");
+ }
+ // This check needs to be done before doStart(),
+ // as doStart() accesses sourceRunner.getSource()
+ Source source = Preconditions.checkNotNull(sourceRunner.getSource(),
+ "Source runner returned null source");
+ if (source instanceof EmbeddedSource) {
+ embeddedSource = (EmbeddedSource)source;
+ } else {
+ throw new IllegalStateException("Unknown source type: " +
source.getClass().getName());
+ }
+ doStart();
+ state = State.STARTED;
+ }
+
+ /**
+ * Stops the agent. Can only be called after a successful call to start().
+ * After a call to stop(), the agent can be re-configured with the
+ * configure() method or re-started with the start() method.
+ *
+ * @throws FlumeException if a component cannot be stopped
+ * @throws IllegalStateException if the agent is not started
+ */
+ public void stop()
+ throws FlumeException {
+ if (state != State.STARTED) {
+ throw new IllegalStateException("Cannot be stopped unless started");
+ }
+ supervisor.stop();
+ embeddedSource = null;
+ state = State.STOPPED;
+ }
+
+ private void doConfigure(Map<String, String> properties) {
+
+ properties = EmbeddedAgentConfiguration.configure(name, properties);
+
+ if (LOGGER.isDebugEnabled() && LogPrivacyUtil.allowLogPrintConfig()) {
+ LOGGER.debug("Agent configuration values");
+ for (String key : new TreeSet<String>(properties.keySet())) {
+ LOGGER.debug(key + " = " + properties.get(key));
+ }
+ }
+
+ MaterializedConfiguration conf = configurationProvider.get(name,
+ properties);
+ Map<String, SourceRunner> sources = conf.getSourceRunners();
+ if (sources.size() != 1) {
+ throw new FlumeException("Expected one source and got " +
+ sources.size());
+ }
+ Map<String, Channel> channels = conf.getChannels();
+ if (channels.size() != 1) {
+ throw new FlumeException("Expected one channel and got " +
+ channels.size());
+ }
+ Map<String, SinkRunner> sinks = conf.getSinkRunners();
+ if (sinks.size() != 1) {
+ throw new FlumeException("Expected one sink group and got " +
+ sinks.size());
+ }
+ this.sourceRunner = sources.values().iterator().next();
+ this.channel = channels.values().iterator().next();
+ this.sinkRunner = sinks.values().iterator().next();
+ }
+
+ /**
+ * Adds event to the channel owned by the agent. Note however, that the
+ * event is not copied and as such, the byte array and headers cannot
+ * be re-used by the caller.
+ * @param event
+ * @throws EventDeliveryException if unable to add event to channel
+ */
+ public void put(Event event) throws EventDeliveryException {
+ if (state != State.STARTED) {
+ throw new IllegalStateException("Cannot put events unless started");
+ }
+ try {
+ embeddedSource.put(event);
+ } catch (ChannelException ex) {
+ throw new EventDeliveryException("Embedded agent " + name +
+ ": Unable to process event: " + ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * Adds events to the channel owned by the agent. Note however, that the
+ * event is not copied and as such, the byte array and headers cannot
+ * be re-used by the caller.
+ * @param events
+ * @throws EventDeliveryException if unable to add event to channel
+ */
+ public void putAll(List<Event> events) throws EventDeliveryException {
+ if (state != State.STARTED) {
+ throw new IllegalStateException("Cannot put events unless started");
+ }
+ try {
+ embeddedSource.putAll(events);
+ } catch (ChannelException ex) {
+ throw new EventDeliveryException("Embedded agent " + name +
+ ": Unable to process event: " + ex.getMessage(), ex);
+ }
+ }
+
+ private void doStart() {
+ boolean error = true;
+ try {
+ channel.start();
+ sinkRunner.start();
+ sourceRunner.start();
+
+ supervisor.supervise(channel,
+ new SupervisorPolicy.AlwaysRestartPolicy(), LifecycleState.START);
+ supervisor.supervise(sinkRunner,
+ new SupervisorPolicy.AlwaysRestartPolicy(), LifecycleState.START);
+ supervisor.supervise(sourceRunner,
+ new SupervisorPolicy.AlwaysRestartPolicy(), LifecycleState.START);
+ error = false;
+ } finally {
+ if (error) {
+ stopLogError(sourceRunner);
+ stopLogError(channel);
+ stopLogError(sinkRunner);
+ supervisor.stop();
+ }
+ }
+ }
+
+ private void stopLogError(LifecycleAware lifeCycleAware) {
+ try {
+ if (LifecycleState.START.equals(lifeCycleAware.getLifecycleState())) {
+ lifeCycleAware.stop();
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Exception while stopping " + lifeCycleAware, e);
+ }
+ }
+
+ private static enum State {
+ NEW(),
+ STOPPED(),
+ STARTED();
+ }
+}
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgentConfiguration.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgentConfiguration.java
new file mode 100644
index 0000000..fe728f9
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedAgentConfiguration.java
@@ -0,0 +1,323 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.flume.FlumeException;
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.conf.BasicConfigurationConstants;
+import org.apache.flume.conf.channel.ChannelType;
+import org.apache.flume.conf.sink.SinkProcessorType;
+import org.apache.flume.conf.sink.SinkType;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+
+/**
+ * Stores publicly accessible configuration constants and private
+ * configuration constants and methods.
+ */
[email protected]
[email protected]
+public class EmbeddedAgentConfiguration {
+ public static final String SEPERATOR = ".";
+ private static final Joiner JOINER = Joiner.on(SEPERATOR);
+ private static final String TYPE = "type";
+
+ /**
+ * Prefix for source properties
+ */
+ public static final String SOURCE = "source";
+ /**
+ * Prefix for channel properties
+ */
+ public static final String CHANNEL = "channel";
+ /**
+ * Prefix for sink processor properties
+ */
+ public static final String SINK_PROCESSOR = "processor";
+ /**
+ * Space delimited list of sink names: e.g. sink1 sink2 sink3
+ */
+ public static final String SINKS = "sinks";
+
+ public static final String SINKS_PREFIX = join(SINKS, "");
+ /**
+ * Source type, choices are `embedded'
+ */
+ public static final String SOURCE_TYPE = join(SOURCE, TYPE);
+ /**
+ * Prefix for passing configuration parameters to the source
+ */
+ public static final String SOURCE_PREFIX = join(SOURCE, "");
+ /**
+ * Channel type, choices are `memory' or `file'
+ */
+ public static final String CHANNEL_TYPE = join(CHANNEL, TYPE);
+ /**
+ * Prefix for passing configuration parameters to the channel
+ */
+ public static final String CHANNEL_PREFIX = join(CHANNEL, "");
+ /**
+ * Sink processor type, choices are `default', `failover' or `load_balance'
+ */
+ public static final String SINK_PROCESSOR_TYPE = join(SINK_PROCESSOR, TYPE);
+ /**
+ * Prefix for passing configuration parameters to the sink processor
+ */
+ public static final String SINK_PROCESSOR_PREFIX = join(SINK_PROCESSOR, "");
+ /**
+ * Embedded source which provides simple in-memory transfer to channel.
+ * Use this source via the put,putAll methods on the EmbeddedAgent. This
+ * is the only supported source to use for Embedded Agents.
+ */
+ public static final String SOURCE_TYPE_EMBEDDED =
EmbeddedSource.class.getName();
+ private static final String SOURCE_TYPE_EMBEDDED_ALIAS = "EMBEDDED";
+ /**
+ * Memory channel which stores events in heap. See Flume User Guide for
+ * configuration information. This is the recommended channel to use for
+ * Embedded Agents.
+ */
+ public static final String CHANNEL_TYPE_MEMORY = ChannelType.MEMORY.name();
+
+ /**
+ * Spillable Memory channel which stores events in heap. See Flume User Guide
for
+ * configuration information. This is the recommended channel to use for
+ * Embedded Agents.
+ */
+ public static final String CHANNEL_TYPE_SPILLABLEMEMORY =
ChannelType.SPILLABLEMEMORY.name();
+
+ /**
+ * File based channel which stores events in on local disk. See Flume User
+ * Guide for configuration information.
+ */
+ public static final String CHANNEL_TYPE_FILE = ChannelType.FILE.name();
+
+ /**
+ * Avro sink which can send events to a downstream avro source. This is the
+ * only supported sink for Embedded Agents.
+ */
+ public static final String SINK_TYPE_AVRO = SinkType.AVRO.name();
+
+ /**
+ * Default sink processors which may be used when there is only a single
sink.
+ */
+ public static final String SINK_PROCESSOR_TYPE_DEFAULT =
SinkProcessorType.DEFAULT.name();
+ /**
+ * Failover sink processor. See Flume User Guide for configuration
+ * information.
+ */
+ public static final String SINK_PROCESSOR_TYPE_FAILOVER =
SinkProcessorType.FAILOVER.name();
+ /**
+ * Load balancing sink processor. See Flume User Guide for configuration
+ * information.
+ */
+ public static final String SINK_PROCESSOR_TYPE_LOAD_BALANCE =
+ SinkProcessorType.LOAD_BALANCE.name();
+
+ private static final String[] ALLOWED_SOURCES = {
+ SOURCE_TYPE_EMBEDDED_ALIAS,
+ SOURCE_TYPE_EMBEDDED,
+ };
+
+ private static final String[] ALLOWED_CHANNELS = {
+ CHANNEL_TYPE_MEMORY,
+ CHANNEL_TYPE_FILE
+ };
+
+ private static final String[] ALLOWED_SINKS = {
+ SINK_TYPE_AVRO
+ };
+
+ private static final String[] ALLOWED_SINK_PROCESSORS = {
+ SINK_PROCESSOR_TYPE_DEFAULT,
+ SINK_PROCESSOR_TYPE_FAILOVER,
+ SINK_PROCESSOR_TYPE_LOAD_BALANCE
+ };
+
+ private static final ImmutableList<String> DISALLOWED_SINK_NAMES =
+ ImmutableList.of("source", "channel", "processor");
+
+ private static void validate(String name,
+ Map<String, String> properties) throws FlumeException {
+
+ if (properties.containsKey(SOURCE_TYPE)) {
+ checkAllowed(ALLOWED_SOURCES, properties.get(SOURCE_TYPE));
+ }
+ checkRequired(properties, CHANNEL_TYPE);
+ checkAllowed(ALLOWED_CHANNELS, properties.get(CHANNEL_TYPE));
+ checkRequired(properties, SINKS);
+ String sinkNames = properties.get(SINKS);
+ for (String sink : sinkNames.split("\\s+")) {
+ if (DISALLOWED_SINK_NAMES.contains(sink.toLowerCase(Locale.ENGLISH))) {
+ throw new FlumeException("Sink name " + sink + " is one of the" +
+ " disallowed sink names: " + DISALLOWED_SINK_NAMES);
+ }
+ String key = join(sink, TYPE);
+ checkRequired(properties, key);
+ checkAllowed(ALLOWED_SINKS, properties.get(key));
+ }
+ checkRequired(properties, SINK_PROCESSOR_TYPE);
+ checkAllowed(ALLOWED_SINK_PROCESSORS, properties.get(SINK_PROCESSOR_TYPE));
+ }
+ /**
+ * Folds embedded configuration structure into an agent configuration.
+ * Should only be called after validate returns without error.
+ *
+ * @param name - agent name
+ * @param properties - embedded agent configuration
+ * @return configuration applicable to a flume agent
+ */
+ @InterfaceAudience.Private
+ @InterfaceStability.Unstable
+ static Map<String, String> configure(String name,
+ Map<String, String> properties) throws FlumeException {
+ validate(name, properties);
+ // we are going to modify the properties as we parse the config
+ properties = new HashMap<String, String>(properties);
+
+ if (!properties.containsKey(SOURCE_TYPE) ||
+
SOURCE_TYPE_EMBEDDED_ALIAS.equalsIgnoreCase(properties.get(SOURCE_TYPE))) {
+ properties.put(SOURCE_TYPE, SOURCE_TYPE_EMBEDDED);
+ }
+ String sinkNames = properties.remove(SINKS);
+
+ String strippedName = name.replaceAll("\\s+","");
+ String sourceName = "source-" + strippedName;
+ String channelName = "channel-" + strippedName;
+ String sinkGroupName = "sink-group-" + strippedName;
+
+ /*
+ * Now we are going to process the user supplied configuration
+ * and generate an agent configuration. This is only to supply
+ * a simpler client api than passing in an entire agent configuration.
+ */
+ // user supplied config -> agent configuration
+ Map<String, String> result = Maps.newHashMap();
+
+ /*
+ * First we are going to setup all the root level pointers. I.E
+ * point the agent at the components, sink group at sinks, and
+ * source at the channel.
+ */
+ // point agent at source
+ result.put(join(name, BasicConfigurationConstants.CONFIG_SOURCES),
+ sourceName);
+ // point agent at channel
+ result.put(join(name, BasicConfigurationConstants.CONFIG_CHANNELS),
+ channelName);
+ // point agent at sinks
+ result.put(join(name, BasicConfigurationConstants.CONFIG_SINKS),
+ sinkNames);
+ // points the agent at the sinkgroup
+ result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS),
+ sinkGroupName);
+ // points the sinkgroup at the sinks
+ result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS,
+ sinkGroupName, SINKS), sinkNames);
+ // points the source at the channel
+ result.put(join(name,
+ BasicConfigurationConstants.CONFIG_SOURCES, sourceName,
+ BasicConfigurationConstants.CONFIG_CHANNELS), channelName);
+
+ // Properties will be modified during iteration so we need a
+ // copy of the keys.
+ Set<String> userProvidedKeys = new HashSet<String>(properties.keySet());
+
+ /*
+ * Second process the sink configuration and point the sinks
+ * at the channel.
+ */
+ for (String sink : sinkNames.split("\\s+")) {
+ for (String key : userProvidedKeys) {
+ String value = properties.get(key);
+ if (key.startsWith(sink + SEPERATOR)) {
+ properties.remove(key);
+ result.put(join(name,
+ BasicConfigurationConstants.CONFIG_SINKS, key), value);
+ }
+ }
+ // point the sink at the channel
+ result.put(join(name,
+ BasicConfigurationConstants.CONFIG_SINKS, sink,
+ BasicConfigurationConstants.CONFIG_CHANNEL), channelName);
+ }
+ /*
+ * Third, process all remaining configuration items, prefixing them
+ * correctly and then passing them on to the agent.
+ */
+ userProvidedKeys = new HashSet<String>(properties.keySet());
+ for (String key : userProvidedKeys) {
+ String value = properties.get(key);
+ if (key.startsWith(SOURCE_PREFIX)) {
+ // users use `source' but agent needs the actual source name
+ key = key.replaceFirst(SOURCE, sourceName);
+ result.put(join(name,
+ BasicConfigurationConstants.CONFIG_SOURCES, key), value);
+ } else if (key.startsWith(CHANNEL_PREFIX)) {
+ // users use `channel' but agent needs the actual channel name
+ key = key.replaceFirst(CHANNEL, channelName);
+ result.put(join(name,
+ BasicConfigurationConstants.CONFIG_CHANNELS, key), value);
+ } else if (key.startsWith(SINK_PROCESSOR_PREFIX)) {
+ // agent.sinkgroups.sinkgroup.processor.*
+ result.put(join(name, BasicConfigurationConstants.CONFIG_SINKGROUPS,
+ sinkGroupName, key), value);
+ } else {
+ // XXX should we simply ignore this?
+ throw new FlumeException("Unknown configuration " + key);
+ }
+ }
+ return result;
+ }
+ private static void checkAllowed(String[] allowedTypes, String type) {
+ boolean isAllowed = false;
+ type = type.trim();
+ for (String allowedType : allowedTypes) {
+ if (allowedType.equalsIgnoreCase(type)) {
+ isAllowed = true;
+ break;
+ }
+ }
+ if (!isAllowed) {
+ throw new FlumeException("Component type of " + type + " is not in " +
+ "allowed types of " + Arrays.toString(allowedTypes));
+ }
+ }
+ private static void checkRequired(Map<String, String> properties, String
name) {
+ if (!properties.containsKey(name)) {
+ throw new FlumeException("Required parameter not found " + name);
+ }
+ }
+
+ private static String join(String... parts) {
+ return JOINER.join(parts);
+ }
+
+ private EmbeddedAgentConfiguration() {
+ }
+}
\ No newline at end of file
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedSource.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedSource.java
new file mode 100644
index 0000000..375e27b
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/EmbeddedSource.java
@@ -0,0 +1,52 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.List;
+
+import org.apache.flume.exception.ChannelException;
+import org.apache.flume.Context;
+import org.apache.flume.Event;
+import org.apache.flume.EventDrivenSource;
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.conf.Configurable;
+import org.apache.flume.source.AbstractSource;
+
+/**
+ * Simple source used to allow direct access to the channel for the Embedded
+ * Agent.
+ */
[email protected]
[email protected]
+public class EmbeddedSource extends AbstractSource implements
EventDrivenSource, Configurable {
+
+ @Override
+ public void configure(Context context) {
+ }
+
+ public void put(Event event) throws ChannelException {
+ getChannelProcessor().processEvent(event);
+ }
+
+ public void putAll(List<Event> events) throws ChannelException {
+ getChannelProcessor().processEventBatch(events);
+ }
+
+}
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MaterializedConfigurationProvider.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MaterializedConfigurationProvider.java
new file mode 100644
index 0000000..9d93138
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MaterializedConfigurationProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.Map;
+
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.node.MaterializedConfiguration;
+
+/**
+ * Provides {@see MaterializedConfiguration} for a given agent and set of
+ * properties. This class exists simply to make more easily testable. That is
+ * it allows us to mock the actual Source, Sink, and Channel components
+ * as opposed to instantiation of real components.
+ */
[email protected]
[email protected]
+class MaterializedConfigurationProvider {
+
+ MaterializedConfiguration get(String name, Map<String, String> properties) {
+ MemoryConfigurationProvider confProvider =
+ new MemoryConfigurationProvider(name, properties);
+ return confProvider.getConfiguration();
+ }
+}
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MemoryConfigurationProvider.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MemoryConfigurationProvider.java
new file mode 100644
index 0000000..0377501
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/MemoryConfigurationProvider.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.Map;
+
+import org.apache.flume.annotations.InterfaceAudience;
+import org.apache.flume.annotations.InterfaceStability;
+import org.apache.flume.conf.FlumeConfiguration;
+import org.apache.flume.node.AbstractConfigurationProvider;
+
+/**
+ * MemoryConfigurationProvider is the simplest possible
+ * AbstractConfigurationProvider simply turning a give properties file and
+ * agent name into a FlumeConfiguration object.
+ */
[email protected]
[email protected]
+class MemoryConfigurationProvider extends AbstractConfigurationProvider {
+ private final Map<String, String> properties;
+
+ MemoryConfigurationProvider(String name, Map<String, String> properties) {
+ super(name);
+ this.properties = properties;
+ }
+
+ @Override
+ protected FlumeConfiguration getFlumeConfiguration() {
+ return new FlumeConfiguration(properties);
+ }
+
+}
diff --git
a/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/package-info.java
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/package-info.java
new file mode 100644
index 0000000..919a630
--- /dev/null
+++
b/flume-embedded-agent/src/main/java/org/apache/flume/agent/embedded/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ * This package provides Flume users the ability to embed simple agents
+ * in applications. For specific and up to date information, please see
+ * the Flume Developer Guide.
+ */
+package org.apache.flume.agent.embedded;
diff --git
a/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgent.java
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgent.java
new file mode 100644
index 0000000..68a3a07
--- /dev/null
+++
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgent.java
@@ -0,0 +1,230 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.avro.ipc.netty.NettyServer;
+import org.apache.avro.ipc.Responder;
+import org.apache.avro.ipc.specific.SpecificResponder;
+import org.apache.flume.Event;
+import org.apache.flume.event.EventBuilder;
+import org.apache.flume.source.avro.AvroFlumeEvent;
+import org.apache.flume.source.avro.AvroSourceProtocol;
+import org.apache.flume.source.avro.Status;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class TestEmbeddedAgent {
+ private static final Logger LOGGER = LoggerFactory
+ .getLogger(TestEmbeddedAgent.class);
+ private static final String HOSTNAME = "localhost";
+ private static AtomicInteger serialNumber = new AtomicInteger(0);
+ private EmbeddedAgent agent;
+ private Map<String, String> properties;
+ private EventCollector eventCollector;
+ private NettyServer nettyServer;
+ private Map<String, String> headers;
+ private byte[] body;
+
+ @Before
+ public void setUp() throws Exception {
+ headers = Maps.newHashMap();
+ headers.put("key1", "value1");
+ body = "body".getBytes(Charsets.UTF_8);
+
+ int port = findFreePort();
+ eventCollector = new EventCollector();
+ Responder responder = new SpecificResponder(AvroSourceProtocol.class,
+ eventCollector);
+ nettyServer = new NettyServer(responder,
+ new InetSocketAddress(HOSTNAME, port));
+ nettyServer.start();
+
+ // give the server a second to start
+ Thread.sleep(1000L);
+
+ properties = Maps.newHashMap();
+ properties.put("channel.type", "memory");
+ properties.put("channel.capacity", "200");
+ properties.put("sinks", "sink1 sink2");
+ properties.put("sink1.type", "avro");
+ properties.put("sink2.type", "avro");
+ properties.put("sink1.hostname", HOSTNAME);
+ properties.put("sink1.port", String.valueOf(port));
+ properties.put("sink2.hostname", HOSTNAME);
+ properties.put("sink2.port", String.valueOf(port));
+ properties.put("processor.type", "load_balance");
+
+ agent = new EmbeddedAgent("test-" + serialNumber.incrementAndGet());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (agent != null) {
+ try {
+ agent.stop();
+ } catch (Exception e) {
+ LOGGER.debug("Error shutting down agent", e);
+ }
+ }
+ if (nettyServer != null) {
+ try {
+ nettyServer.close();
+ } catch (Exception e) {
+ LOGGER.debug("Error shutting down server", e);
+ }
+ }
+ }
+
+ @Test(timeout = 30000L)
+ public void testPut() throws Exception {
+ agent.configure(properties);
+ agent.start();
+ agent.put(EventBuilder.withBody(body, headers));
+
+ Event event;
+ while ((event = eventCollector.poll()) == null) {
+ Thread.sleep(500L);
+ }
+ Assert.assertNotNull(event);
+ Assert.assertArrayEquals(body, event.getBody());
+ Assert.assertEquals(headers, event.getHeaders());
+ }
+
+ @Test(timeout = 30000L)
+ public void testPutAll() throws Exception {
+ List<Event> events = Lists.newArrayList();
+ events.add(EventBuilder.withBody(body, headers));
+ agent.configure(properties);
+ agent.start();
+ agent.putAll(events);
+
+ Event event;
+ while ((event = eventCollector.poll()) == null) {
+ Thread.sleep(500L);
+ }
+ Assert.assertNotNull(event);
+ Assert.assertArrayEquals(body, event.getBody());
+ Assert.assertEquals(headers, event.getHeaders());
+ }
+
+ @Test(timeout = 30000L)
+ public void testPutWithInterceptors() throws Exception {
+ properties.put("source.interceptors", "i1");
+ properties.put("source.interceptors.i1.type", "static");
+ properties.put("source.interceptors.i1.key", "key2");
+ properties.put("source.interceptors.i1.value", "value2");
+
+ agent.configure(properties);
+ agent.start();
+ agent.put(EventBuilder.withBody(body, headers));
+
+ Event event;
+ while ((event = eventCollector.poll()) == null) {
+ Thread.sleep(500L);
+ }
+ Assert.assertNotNull(event);
+ Assert.assertArrayEquals(body, event.getBody());
+ Map<String, String> newHeaders = new HashMap<String, String>(headers);
+ newHeaders.put("key2", "value2");
+ Assert.assertEquals(newHeaders, event.getHeaders());
+ }
+
+
+ @Test(timeout = 30000L)
+ public void testEmbeddedAgentName() throws Exception {
+ EmbeddedAgent embedAgent = new EmbeddedAgent("test 1 2" +
serialNumber.incrementAndGet());
+ List<Event> events = Lists.newArrayList();
+ events.add(EventBuilder.withBody(body, headers));
+ embedAgent.configure(properties);
+ embedAgent.start();
+ embedAgent.putAll(events);
+
+ Event event;
+ while ((event = eventCollector.poll()) == null) {
+ Thread.sleep(500L);
+ }
+ Assert.assertNotNull(event);
+ Assert.assertArrayEquals(body, event.getBody());
+ Assert.assertEquals(headers, event.getHeaders());
+ if (embedAgent != null) {
+ try {
+ embedAgent.stop();
+ } catch (Exception e) {
+ LOGGER.debug("Error shutting down agent", e);
+ }
+ }
+ }
+
+ static class EventCollector implements AvroSourceProtocol {
+ private final Queue<AvroFlumeEvent> eventQueue =
+ new LinkedBlockingQueue<AvroFlumeEvent>();
+
+ public Event poll() {
+ AvroFlumeEvent avroEvent = eventQueue.poll();
+ if (avroEvent != null) {
+ return EventBuilder.withBody(avroEvent.getBody().array(),
+ toStringMap(avroEvent.getHeaders()));
+ }
+ return null;
+ }
+ @Override
+ public Status append(AvroFlumeEvent event) {
+ eventQueue.add(event);
+ return Status.OK;
+ }
+ @Override
+ public Status appendBatch(List<AvroFlumeEvent> events) {
+ Preconditions.checkState(eventQueue.addAll(events));
+ return Status.OK;
+ }
+ }
+
+ private static Map<String, String> toStringMap(Map<CharSequence,
CharSequence> charSeqMap) {
+ Map<String, String> stringMap = new HashMap<String, String>();
+ for (Map.Entry<CharSequence, CharSequence> entry : charSeqMap.entrySet()) {
+ stringMap.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ return stringMap;
+ }
+
+ private static int findFreePort() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }
+}
diff --git
a/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentConfiguration.java
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentConfiguration.java
new file mode 100644
index 0000000..ed26294
--- /dev/null
+++
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentConfiguration.java
@@ -0,0 +1,151 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import java.util.Map;
+
+import junit.framework.Assert;
+
+import org.apache.flume.FlumeException;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Maps;
+
+public class TestEmbeddedAgentConfiguration {
+ private Map<String, String> properties;
+
+ @Before
+ public void setUp() throws Exception {
+ properties = Maps.newHashMap();
+ properties.put("source.type",
EmbeddedAgentConfiguration.SOURCE_TYPE_EMBEDDED);
+ properties.put("channel.type", "memory");
+ properties.put("channel.capacity", "200");
+ properties.put("sinks", "sink1 sink2");
+ properties.put("sink1.type", "avro");
+ properties.put("sink2.type", "avro");
+ properties.put("sink1.hostname", "sink1.host");
+ properties.put("sink1.port", "2");
+ properties.put("sink2.hostname", "sink2.host");
+ properties.put("sink2.port", "2");
+ properties.put("processor.type", "load_balance");
+ properties.put("source.interceptors", "i1");
+ properties.put("source.interceptors.i1.type", "timestamp");
+ }
+
+ @Test
+ public void testFullSourceType() throws Exception {
+ doTestExcepted(EmbeddedAgentConfiguration.configure("test1", properties));
+ }
+
+ @Test
+ public void testMissingSourceType() throws Exception {
+ Assert.assertNotNull(properties.remove("source.type"));
+ doTestExcepted(EmbeddedAgentConfiguration.configure("test1", properties));
+ }
+
+ @Test
+ public void testShortSourceType() throws Exception {
+ properties.put("source.type", "EMBEDDED");
+ doTestExcepted(EmbeddedAgentConfiguration.configure("test1", properties));
+ }
+
+ public void doTestExcepted(Map<String, String> actual) throws Exception {
+ Map<String, String> expected = Maps.newHashMap();
+ expected.put("test1.channels", "channel-test1");
+ expected.put("test1.channels.channel-test1.capacity", "200");
+ expected.put("test1.channels.channel-test1.type", "memory");
+ expected.put("test1.sinkgroups", "sink-group-test1");
+ expected.put("test1.sinkgroups.sink-group-test1.processor.type",
"load_balance");
+ expected.put("test1.sinkgroups.sink-group-test1.sinks", "sink1 sink2");
+ expected.put("test1.sinks", "sink1 sink2");
+ expected.put("test1.sinks.sink1.channel", "channel-test1");
+ expected.put("test1.sinks.sink1.hostname", "sink1.host");
+ expected.put("test1.sinks.sink1.port", "2");
+ expected.put("test1.sinks.sink1.type", "avro");
+ expected.put("test1.sinks.sink2.channel", "channel-test1");
+ expected.put("test1.sinks.sink2.hostname", "sink2.host");
+ expected.put("test1.sinks.sink2.port", "2");
+ expected.put("test1.sinks.sink2.type", "avro");
+ expected.put("test1.sources", "source-test1");
+ expected.put("test1.sources.source-test1.channels", "channel-test1");
+ expected.put("test1.sources.source-test1.type",
+ EmbeddedAgentConfiguration.SOURCE_TYPE_EMBEDDED);
+ expected.put("test1.sources.source-test1.interceptors", "i1");
+ expected.put("test1.sources.source-test1.interceptors.i1.type",
"timestamp");
+ Assert.assertEquals(expected, actual);
+ }
+
+ @Test(expected = FlumeException.class)
+ public void testBadSource() throws Exception {
+ properties.put("source.type", "exec");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testBadChannel() throws Exception {
+ properties.put("channel.type", "jdbc");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testBadSink() throws Exception {
+ properties.put("sink1.type", "hbase");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testBadSinkProcessor() throws Exception {
+ properties.put("processor.type", "bad");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+
+ @Test(expected = FlumeException.class)
+ public void testNoChannel() throws Exception {
+ properties.remove("channel.type");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testNoSink() throws Exception {
+ properties.remove("sink2.type");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testNoSinkProcessor() throws Exception {
+ properties.remove("processor.type");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testBadKey() throws Exception {
+ properties.put("bad.key.name", "bad");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testSinkNamedLikeSource() throws Exception {
+ properties.put("sinks", "source");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testSinkNamedLikeChannel() throws Exception {
+ properties.put("sinks", "channel");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+ @Test(expected = FlumeException.class)
+ public void testSinkNamedLikeProcessor() throws Exception {
+ properties.put("sinks", "processor");
+ EmbeddedAgentConfiguration.configure("test1", properties);
+ }
+}
\ No newline at end of file
diff --git
a/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentEmbeddedSource.java
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentEmbeddedSource.java
new file mode 100644
index 0000000..ae59444
--- /dev/null
+++
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentEmbeddedSource.java
@@ -0,0 +1,209 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.Assert;
+
+import org.apache.flume.Channel;
+import org.apache.flume.Event;
+import org.apache.flume.EventDeliveryException;
+import org.apache.flume.SinkRunner;
+import org.apache.flume.SourceRunner;
+import org.apache.flume.event.SimpleEvent;
+import org.apache.flume.lifecycle.LifecycleState;
+import org.apache.flume.node.MaterializedConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class TestEmbeddedAgentEmbeddedSource {
+
+ private EmbeddedAgent agent;
+ private Map<String, String> properties;
+
+ private MaterializedConfiguration config;
+ private EmbeddedSource source;
+ private SourceRunner sourceRunner;
+ private Channel channel;
+ private SinkRunner sinkRunner;
+
+ @Before
+ public void setUp() throws Exception {
+
+ properties = Maps.newHashMap();
+ properties.put("source.type",
EmbeddedAgentConfiguration.SOURCE_TYPE_EMBEDDED);
+ properties.put("channel.type", "memory");
+ properties.put("sinks", "sink1 sink2");
+ properties.put("sink1.type", "avro");
+ properties.put("sink2.type", "avro");
+ properties.put("processor.type", "load_balance");
+
+ sourceRunner = mock(SourceRunner.class);
+ channel = mock(Channel.class);
+ sinkRunner = mock(SinkRunner.class);
+
+ source = mock(EmbeddedSource.class);
+ when(sourceRunner.getSource()).thenReturn(source);
+
+ when(sourceRunner.getLifecycleState()).thenReturn(LifecycleState.START);
+ when(channel.getLifecycleState()).thenReturn(LifecycleState.START);
+ when(sinkRunner.getLifecycleState()).thenReturn(LifecycleState.START);
+
+ config = new MaterializedConfiguration() {
+ @Override
+ public Map<String, SourceRunner> getSourceRunners() {
+ Map<String, SourceRunner> result = Maps.newHashMap();
+ result.put("source", sourceRunner);
+ return ImmutableMap.copyOf(result);
+ }
+
+ @Override
+ public Map<String, SinkRunner> getSinkRunners() {
+ Map<String, SinkRunner> result = Maps.newHashMap();
+ result.put("sink", sinkRunner);
+ return ImmutableMap.copyOf(result);
+ }
+
+ @Override
+ public Map<String, Channel> getChannels() {
+ Map<String, Channel> result = Maps.newHashMap();
+ result.put("channel", channel);
+ return ImmutableMap.copyOf(result);
+ }
+
+ @Override
+ public void addSourceRunner(String name, SourceRunner sourceRunner) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addSinkRunner(String name, SinkRunner sinkRunner) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addChannel(String name, Channel channel) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ agent = new EmbeddedAgent(new MaterializedConfigurationProvider() {
+ public MaterializedConfiguration get(String name, Map<String, String>
properties) {
+ return config;
+ }
+ }, "dummy");
+ }
+
+ @Test
+ public void testStart() {
+ agent.configure(properties);
+ agent.start();
+ verify(sourceRunner, times(1)).start();
+ verify(channel, times(1)).start();
+ verify(sinkRunner, times(1)).start();
+ }
+
+ @Test
+ public void testStop() {
+ agent.configure(properties);
+ agent.start();
+ agent.stop();
+ verify(sourceRunner, times(1)).stop();
+ verify(channel, times(1)).stop();
+ verify(sinkRunner, times(1)).stop();
+ }
+
+ @Test
+ public void testStartSourceThrowsException() {
+ doThrow(new LocalRuntimeException()).when(sourceRunner).start();
+ startExpectingLocalRuntimeException();
+ }
+
+ @Test
+ public void testStartChannelThrowsException() {
+ doThrow(new LocalRuntimeException()).when(channel).start();
+ startExpectingLocalRuntimeException();
+ }
+
+ @Test
+ public void testStartSinkThrowsException() {
+ doThrow(new LocalRuntimeException()).when(sinkRunner).start();
+ startExpectingLocalRuntimeException();
+ }
+
+ private void startExpectingLocalRuntimeException() {
+ agent.configure(properties);
+ try {
+ agent.start();
+ Assert.fail();
+ } catch (LocalRuntimeException e) {
+ // expected
+ }
+ verify(sourceRunner, times(1)).stop();
+ verify(channel, times(1)).stop();
+ verify(sinkRunner, times(1)).stop();
+ }
+
+ private static class LocalRuntimeException extends RuntimeException {
+ private static final long serialVersionUID = 116546244849853151L;
+ }
+
+ @Test
+ public void testPut() throws EventDeliveryException {
+ Event event = new SimpleEvent();
+ agent.configure(properties);
+ agent.start();
+ agent.put(event);
+ verify(source, times(1)).put(event);
+ }
+
+ @Test
+ public void testPutAll() throws EventDeliveryException {
+ Event event = new SimpleEvent();
+ List<Event> events = Lists.newArrayList();
+ events.add(event);
+ agent.configure(properties);
+ agent.start();
+ agent.putAll(events);
+ verify(source, times(1)).putAll(events);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testPutNotStarted() throws EventDeliveryException {
+ Event event = new SimpleEvent();
+ agent.configure(properties);
+ agent.put(event);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testPutAllNotStarted() throws EventDeliveryException {
+ Event event = new SimpleEvent();
+ List<Event> events = Lists.newArrayList();
+ events.add(event);
+ agent.configure(properties);
+ agent.putAll(events);
+ }
+}
diff --git
a/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentState.java
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentState.java
new file mode 100644
index 0000000..0f0ad23
--- /dev/null
+++
b/flume-embedded-agent/src/test/java/org/apache/flume/agent/embedded/TestEmbeddedAgentState.java
@@ -0,0 +1,125 @@
+/*
+ * 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 org.apache.flume.agent.embedded;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Maps;
+import org.apache.flume.FlumeException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class TestEmbeddedAgentState {
+ private static final String HOSTNAME = "localhost";
+ private EmbeddedAgent agent;
+ private Map<String, String> properties;
+
+ @Before
+ public void setUp() throws Exception {
+ agent = new EmbeddedAgent("dummy");
+ properties = Maps.newHashMap();
+ properties.put("source.type",
EmbeddedAgentConfiguration.SOURCE_TYPE_EMBEDDED);
+ properties.put("channel.type", "memory");
+ properties.put("sinks", "sink1 sink2");
+ properties.put("sink1.type", "avro");
+ properties.put("sink2.type", "avro");
+ properties.put("sink1.hostname", HOSTNAME);
+ properties.put("sink1.port", "0");
+ properties.put("sink2.hostname", HOSTNAME);
+ properties.put("sink2.port", "0");
+ properties.put("processor.type", "load_balance");
+ }
+
+ @Test(expected = FlumeException.class)
+ public void testConfigureWithBadSourceType() {
+ properties.put(EmbeddedAgentConfiguration.SOURCE_TYPE, "bad");
+ agent.configure(properties);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testConfigureWhileStarted() {
+ try {
+ agent.configure(properties);
+ agent.start();
+ } catch (Exception e) {
+ Throwables.propagate(e);
+ }
+ agent.configure(properties);
+ }
+
+ @Test
+ public void testConfigureMultipleTimes() {
+ agent.configure(properties);
+ agent.configure(properties);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStartWhileStarted() {
+ try {
+ agent.configure(properties);
+ agent.start();
+ } catch (Exception e) {
+ Throwables.propagate(e);
+ }
+ agent.start();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStartUnconfigured() {
+ agent.start();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStopBeforeConfigure() {
+ agent.stop();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStoppedWhileStopped() {
+ try {
+ agent.configure(properties);
+ } catch (Exception e) {
+ Throwables.propagate(e);
+ }
+ agent.stop();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStopAfterStop() {
+ try {
+ agent.configure(properties);
+ agent.start();
+ agent.stop();
+ } catch (Exception e) {
+ Throwables.propagate(e);
+ }
+ agent.stop();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testStopAfterConfigure() {
+ try {
+ agent.configure(properties);
+ } catch (Exception e) {
+ Throwables.propagate(e);
+ }
+ agent.stop();
+ }
+}
diff --git a/flume-embedded-agent/src/test/resources/log4j2.xml
b/flume-embedded-agent/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..fbc44b4
--- /dev/null
+++ b/flume-embedded-agent/src/test/resources/log4j2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+
+-->
+<Configuration status="OFF">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d (%t) [%p - %l] %m%n" />
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="org.apache.flume" level="DEBUG"/>
+ <Root level="INFO">
+ <AppenderRef ref="Console" />
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e32ba4f..bee2b41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,6 +92,7 @@ limitations under the License.
<modules>
<module>flume-rpc-avro</module>
<module>flume-rpc-thrift</module>
+ <module>flume-embedded-agent</module>
</modules>
<build>
<pluginManagement>