/* * 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.nifi.processors.riemann; import com.aphyr.riemann.Proto; import com.aphyr.riemann.Proto.Event; import com.aphyr.riemann.client.RiemannClient; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.DynamicProperty; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.SupportsBatching; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.annotation.lifecycle.OnStopped; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.PropertyValue; import org.apache.nifi.components.Validator; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Tags({"riemann", "monitoring", "metrics"}) @DynamicProperty(name = "Custom Event Attribute", supportsExpressionLanguage = true, description = "These values will be attached to the Riemann event as a custom attribute", value = "Any value or expression") @CapabilityDescription("Send events to Riemann (http://riemann.io) when FlowFiles pass through this processor. " + "You can use events to notify Riemann that a FlowFile passed through, or you can attach a more " + "meaningful metric, such as, the time a FlowFile took to get to this processor. All attributes attached to " + "events support the NiFi Expression Language.") @SupportsBatching @InputRequirement(Requirement.INPUT_REQUIRED) public class PutRiemann extends AbstractProcessor { protected enum Transport { TCP, UDP } protected volatile RiemannClient riemannClient = null; protected volatile Transport transport; public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("Metrics successfully written to Riemann") .build(); public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("Metrics which failed to write to Riemann") .build(); public static final PropertyDescriptor RIEMANN_HOST = new PropertyDescriptor.Builder() .name("Riemann Address") .description("Hostname of Riemann server") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); public static final PropertyDescriptor RIEMANN_PORT = new PropertyDescriptor.Builder() .name("Riemann Port") .description("Port that Riemann is listening on") .required(true) .defaultValue("5555") .addValidator(StandardValidators.PORT_VALIDATOR) .build(); public static final PropertyDescriptor TRANSPORT_PROTOCOL = new PropertyDescriptor.Builder() .name("Transport Protocol") .description("Transport protocol to speak to Riemann in") .required(true) .allowableValues(new Transport[]{Transport.TCP, Transport.UDP}) .defaultValue("TCP") .build(); public static final PropertyDescriptor BATCH_SIZE = new PropertyDescriptor.Builder() .name("Batch Size") .description("Batch size for incoming FlowFiles") .required(false) .defaultValue("100") .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) .build(); // Attributes Mappings public static final PropertyDescriptor ATTR_SERVICE = new PropertyDescriptor.Builder() .name("Service") .description("Name of service associated to this event (e.g. FTP File Fetched)") .required(false) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .build(); public static final PropertyDescriptor ATTR_STATE = new PropertyDescriptor.Builder() .name("State") .description("State of service associated to this event in string form (e.g. ok, warning, foo)") .required(false) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .build(); public static final PropertyDescriptor ATTR_TIME = new PropertyDescriptor.Builder() .name("Time") .description("Time of event in unix epoch seconds (long), default: (current time)") .required(false) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .expressionLanguageSupported(true) .build(); public static final PropertyDescriptor ATTR_HOST = new PropertyDescriptor.Builder() .name("Host") .description("A hostname associated to this event (e.g. nifi-app1)") .required(false) .defaultValue("${hostname()}") .expressionLanguageSupported(true) .addValidator(Validator.VALID) .build(); public static final PropertyDescriptor ATTR_TTL = new PropertyDescriptor.Builder() .name("TTL") .description("Floating point value in seconds until Riemann considers this event as \"expired\"") .required(false) .addValidator(Validator.VALID) .expressionLanguageSupported(true) .build(); public static final PropertyDescriptor ATTR_METRIC = new PropertyDescriptor.Builder() .name("Metric") .description("Floating point number associated to this event") .required(false) .addValidator(Validator.VALID) .expressionLanguageSupported(true) .build(); public static final PropertyDescriptor ATTR_DESCRIPTION = new PropertyDescriptor.Builder() .name("Description") .description("Description associated to the event") .required(false) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .build(); public static final PropertyDescriptor ATTR_TAGS = new PropertyDescriptor.Builder() .name("Tags") .description("Comma separated list of tags associated to the event") .required(false) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .build(); public static final PropertyDescriptor TIMEOUT = new PropertyDescriptor.Builder() .name("Timeout") .description("Timeout in milliseconds when writing events to Riemann") .required(true) .defaultValue("1000") .addValidator(StandardValidators.POSITIVE_LONG_VALIDATOR) .build(); private volatile List<PropertyDescriptor> customAttributes = new ArrayList<>(); private static final Set<Relationship> RELATIONSHIPS = new HashSet<>(); private static final List<PropertyDescriptor> LOCAL_PROPERTIES = new ArrayList<>(); private volatile int batchSize = -1; private volatile long writeTimeout = 1000; static { RELATIONSHIPS.add(REL_SUCCESS); RELATIONSHIPS.add(REL_FAILURE); LOCAL_PROPERTIES.add(RIEMANN_HOST); LOCAL_PROPERTIES.add(RIEMANN_PORT); LOCAL_PROPERTIES.add(TRANSPORT_PROTOCOL); LOCAL_PROPERTIES.add(TIMEOUT); LOCAL_PROPERTIES.add(BATCH_SIZE); LOCAL_PROPERTIES.add(ATTR_DESCRIPTION); LOCAL_PROPERTIES.add(ATTR_SERVICE); LOCAL_PROPERTIES.add(ATTR_STATE); LOCAL_PROPERTIES.add(ATTR_METRIC); LOCAL_PROPERTIES.add(ATTR_TTL); LOCAL_PROPERTIES.add(ATTR_TAGS); LOCAL_PROPERTIES.add(ATTR_HOST); LOCAL_PROPERTIES.add(ATTR_TIME); } @Override public Set<Relationship> getRelationships() { return RELATIONSHIPS; } @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return LOCAL_PROPERTIES; } @Override protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { return new PropertyDescriptor.Builder() .name(propertyDescriptorName) .expressionLanguageSupported(true) .addValidator(Validator.VALID) .required(false) .dynamic(true) .build(); } @OnStopped public final void cleanUpClient() { if (riemannClient != null) { this.riemannClient.close(); } this.riemannClient = null; this.batchSize = -1; this.customAttributes.clear(); } @OnScheduled public void onScheduled(ProcessContext context) throws ProcessException { if (batchSize == -1) { batchSize = context.getProperty(BATCH_SIZE).asInteger(); } if (riemannClient == null || !riemannClient.isConnected()) { transport = Transport.valueOf(context.getProperty(TRANSPORT_PROTOCOL).getValue()); String host = context.getProperty(RIEMANN_HOST).getValue().trim(); int port = context.getProperty(RIEMANN_PORT).asInteger(); writeTimeout = context.getProperty(TIMEOUT).asLong(); RiemannClient client = null; try { switch (transport) { case TCP: client = RiemannClient.tcp(host, port); break; case UDP: client = RiemannClient.udp(host, port); break; } client.connect(); riemannClient = client; } catch (IOException e) { if (client != null) { client.close(); } context.yield(); throw new ProcessException(String.format("Unable to connect to Riemann [%s:%d] (%s)\n%s", host, port, transport, e.getMessage())); } } if (customAttributes.size() == 0) { for (Map.Entry<PropertyDescriptor, String> property : context.getProperties().entrySet()) { // only custom defined properties if (!getSupportedPropertyDescriptors().contains(property.getKey())) { customAttributes.add(property.getKey()); } } } } @Override public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException { // Check if the client is currently connected, as a previous trigger could have detected a failure // in the connection. if (riemannClient == null || !riemannClient.isConnected()) { // clean up the client and attempt to re-initialize the processor cleanUpClient(); onScheduled(context); } List<FlowFile> incomingFlowFiles = session.get(batchSize); List<FlowFile> successfulFlowFiles = new ArrayList<>(incomingFlowFiles.size()); List<Event> eventsQueue = new ArrayList<>(incomingFlowFiles.size()); for (FlowFile flowFile : incomingFlowFiles) { try { eventsQueue.add(FlowFileToEvent.fromAttributes(context, customAttributes, flowFile)); successfulFlowFiles.add(flowFile); } catch (NumberFormatException e) { getLogger().warn("Unable to create Riemann event.", e); session.transfer(flowFile, REL_FAILURE); } } try { if (transport == Transport.TCP) { Proto.Msg returnMessage = riemannClient.sendEvents(eventsQueue).deref(writeTimeout, TimeUnit.MILLISECONDS); if (returnMessage == null) { context.yield(); throw new ProcessException("Timed out writing to Riemann!"); } } else { riemannClient.sendEvents(eventsQueue); } riemannClient.flush(); session.transfer(successfulFlowFiles, REL_SUCCESS); session.commit(); } catch (Exception e) { context.yield(); session.transfer(incomingFlowFiles); session.commit(); throw new ProcessException("Failed writing to Riemann\n" + e.getMessage()); } } /** * Converts a FlowFile into a Riemann Protobuf Event */ private static class FlowFileToEvent { protected static Event fromAttributes(ProcessContext context, List<PropertyDescriptor> customProperties, FlowFile flowFile) { Event.Builder builder = Event.newBuilder(); PropertyValue service = context.getProperty(ATTR_SERVICE).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(service.getValue())) { builder.setService(service.getValue()); } PropertyValue description = context.getProperty(ATTR_DESCRIPTION).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(description.getValue())) { builder.setDescription(description.getValue()); } PropertyValue metric = context.getProperty(ATTR_METRIC).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(metric.getValue())) { builder.setMetricF(metric.asFloat()); } PropertyValue time = context.getProperty(ATTR_TIME).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(time.getValue())) { builder.setTime(time.asLong()); } PropertyValue state = context.getProperty(ATTR_STATE).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(state.getValue())) { builder.setState(state.getValue()); } PropertyValue ttl = context.getProperty(ATTR_TTL).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(ttl.getValue())) { builder.setTtl(ttl.asFloat()); } PropertyValue host = context.getProperty(ATTR_HOST).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(host.getValue())) { builder.setHost(host.getValue()); } PropertyValue tags = context.getProperty(ATTR_TAGS).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(tags.getValue())) { String[] splitTags = tags.getValue().split(","); for (String splitTag : splitTags) { builder.addTags(splitTag.trim()); } } PropertyValue customAttributeValue; for (PropertyDescriptor customProperty : customProperties) { customAttributeValue = context.getProperty(customProperty).evaluateAttributeExpressions(flowFile); if (StringUtils.isNotBlank(customAttributeValue.getValue())) { builder.addAttributes(Proto.Attribute.newBuilder() .setKey(customProperty.getName()) .setValue(customAttributeValue.getValue()) .build()); } } return builder.build(); } } }