/*
* 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.controller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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.components.PropertyDescriptor;
import org.apache.nifi.controller.status.ConnectionStatus;
import org.apache.nifi.controller.status.ProcessGroupStatus;
import org.apache.nifi.controller.status.ProcessorStatus;
import org.apache.nifi.reporting.AbstractReportingTask;
import org.apache.nifi.reporting.ReportingContext;
import org.apache.nifi.util.FormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Tags({"stats", "log"})
@CapabilityDescription("Logs the 5-minute stats that are shown in the NiFi Summary Page for Processors and Connections, as"
+ " well optionally logging the deltas between the previous iteration and the current iteration. Processors' stats are"
+ " logged using the org.apache.nifi.controller.ControllerStatusReportingTask.Processors logger, while Connections' stats are"
+ " logged using the org.apache.nifi.controller.ControllerStatusReportingTask.Connections logger. These can be configured"
+ " in the NiFi logging configuration to log to different files, if desired.")
public class ControllerStatusReportingTask extends AbstractReportingTask {
public static final PropertyDescriptor SHOW_DELTAS = new PropertyDescriptor.Builder()
.name("Show Deltas")
.description("Specifies whether or not to show the difference in values between the current status and the previous status")
.required(true)
.allowableValues("true", "false")
.defaultValue("true")
.build();
private static final Logger processorLogger = LoggerFactory.getLogger(ControllerStatusReportingTask.class.getName() + ".Processors");
private static final Logger connectionLogger = LoggerFactory.getLogger(ControllerStatusReportingTask.class.getName() + ".Connections");
private static final String PROCESSOR_LINE_FORMAT_NO_DELTA = "| %1$-30.30s | %2$-36.36s | %3$-24.24s | %4$10.10s | %5$19.19s | %6$19.19s | %7$12.12s | %8$13.13s | %9$5.5s | %10$12.12s |\n";
private static final String PROCESSOR_LINE_FORMAT_WITH_DELTA = "| %1$-30.30s | %2$-36.36s | %3$-24.24s | %4$10.10s | %5$43.43s | %6$43.43s | %7$28.28s | %8$30.30s | %9$14.14s | %10$28.28s |\n";
private static final String CONNECTION_LINE_FORMAT_NO_DELTA = "| %1$-36.36s | %2$-30.30s | %3$-36.36s | %4$-30.30s | %5$19.19s | %6$19.19s | %7$19.19s |\n";
private static final String CONNECTION_LINE_FORMAT_WITH_DELTA = "| %1$-36.36s | %2$-30.30s | %3$-36.36s | %4$-30.30s | %5$43.43s | %6$43.43s | %7$43.43s |\n";
private volatile String processorLineFormat;
private volatile String processorHeader;
private volatile String processorBorderLine;
private volatile String connectionLineFormat;
private volatile String connectionHeader;
private volatile String connectionBorderLine;
private volatile Map<String, ProcessorStatus> lastProcessorStatus = new HashMap<>();
private volatile Map<String, ConnectionStatus> lastConnectionStatus = new HashMap<>();
@Override
public final List<PropertyDescriptor> getSupportedPropertyDescriptors() {
final List<PropertyDescriptor> descriptors = new ArrayList<>();
descriptors.add(SHOW_DELTAS);
return descriptors;
}
@OnScheduled
public void onConfigured(final ConfigurationContext context) {
connectionLineFormat = context.getProperty(SHOW_DELTAS).asBoolean() ? CONNECTION_LINE_FORMAT_WITH_DELTA : CONNECTION_LINE_FORMAT_NO_DELTA;
connectionHeader = String.format(connectionLineFormat, "Connection ID", "Source", "Connection Name", "Destination", "Flow Files In", "Flow Files Out", "FlowFiles Queued");
final StringBuilder connectionBorderBuilder = new StringBuilder(connectionHeader.length());
for (int i = 0; i < connectionHeader.length(); i++) {
connectionBorderBuilder.append('-');
}
connectionBorderLine = connectionBorderBuilder.toString();
processorLineFormat = context.getProperty(SHOW_DELTAS).asBoolean() ? PROCESSOR_LINE_FORMAT_WITH_DELTA : PROCESSOR_LINE_FORMAT_NO_DELTA;
processorHeader = String.format(processorLineFormat, "Processor Name", "Processor ID", "Processor Type", "Run Status", "Flow Files In",
"Flow Files Out", "Bytes Read", "Bytes Written", "Tasks", "Proc Time");
final StringBuilder processorBorderBuilder = new StringBuilder(processorHeader.length());
for (int i = 0; i < processorHeader.length(); i++) {
processorBorderBuilder.append('-');
}
processorBorderLine = processorBorderBuilder.toString();
}
@Override
public void onTrigger(final ReportingContext context) {
final ProcessGroupStatus controllerStatus = context.getEventAccess().getControllerStatus();
final boolean showDeltas = context.getProperty(SHOW_DELTAS).asBoolean();
final StringBuilder builder = new StringBuilder();
builder.append("Processor Statuses:\n");
builder.append(processorBorderLine);
builder.append("\n");
builder.append(processorHeader);
builder.append(processorBorderLine);
builder.append("\n");
printProcessorStatus(controllerStatus, builder, showDeltas);
builder.append(processorBorderLine);
processorLogger.info(builder.toString());
builder.setLength(0);
builder.append("Connection Statuses:\n");
builder.append(connectionBorderLine);
builder.append("\n");
builder.append(connectionHeader);
builder.append(connectionBorderLine);
builder.append("\n");
printConnectionStatus(controllerStatus, builder, showDeltas);
builder.append(connectionBorderLine);
connectionLogger.info(builder.toString());
}
private void populateConnectionStatuses(final ProcessGroupStatus groupStatus, final List<ConnectionStatus> statuses) {
statuses.addAll(groupStatus.getConnectionStatus());
for (final ProcessGroupStatus childGroupStatus : groupStatus.getProcessGroupStatus()) {
populateConnectionStatuses(childGroupStatus, statuses);
}
}
private void populateProcessorStatuses(final ProcessGroupStatus groupStatus, final List<ProcessorStatus> statuses) {
statuses.addAll(groupStatus.getProcessorStatus());
for (final ProcessGroupStatus childGroupStatus : groupStatus.getProcessGroupStatus()) {
populateProcessorStatuses(childGroupStatus, statuses);
}
}
// Recursively prints the status of all connections in this group.
private void printConnectionStatus(final ProcessGroupStatus groupStatus, final StringBuilder builder, final boolean showDeltas) {
final List<ConnectionStatus> connectionStatuses = new ArrayList<>();
populateConnectionStatuses(groupStatus, connectionStatuses);
Collections.sort(connectionStatuses, new Comparator<ConnectionStatus>() {
@Override
public int compare(final ConnectionStatus o1, final ConnectionStatus o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return 1;
}
if (o2 == null) {
return -1;
}
return -Long.compare(o1.getQueuedBytes(), o2.getQueuedBytes());
}
});
for (final ConnectionStatus connectionStatus : connectionStatuses) {
final String input = connectionStatus.getInputCount() + " / " + FormatUtils.formatDataSize(connectionStatus.getInputBytes());
final String output = connectionStatus.getOutputCount() + " / " + FormatUtils.formatDataSize(connectionStatus.getOutputBytes());
final String queued = connectionStatus.getQueuedCount() + " / " + FormatUtils.formatDataSize(connectionStatus.getQueuedBytes());
final String inputDiff;
final String outputDiff;
final String queuedDiff;
final ConnectionStatus lastStatus = lastConnectionStatus.get(connectionStatus.getId());
if (showDeltas && lastStatus != null) {
inputDiff = toDiff(lastStatus.getInputCount(), lastStatus.getInputBytes(), connectionStatus.getInputCount(), connectionStatus.getInputBytes());
outputDiff = toDiff(lastStatus.getOutputCount(), lastStatus.getOutputBytes(), connectionStatus.getOutputCount(), connectionStatus.getOutputBytes());
queuedDiff = toDiff(lastStatus.getQueuedCount(), lastStatus.getQueuedBytes(), connectionStatus.getQueuedCount(), connectionStatus.getQueuedBytes());
} else {
inputDiff = toDiff(0L, 0L, connectionStatus.getInputCount(), connectionStatus.getInputBytes());
outputDiff = toDiff(0L, 0L, connectionStatus.getOutputCount(), connectionStatus.getOutputBytes());
queuedDiff = toDiff(0L, 0L, connectionStatus.getQueuedCount(), connectionStatus.getQueuedBytes());
}
if (showDeltas) {
builder.append(String.format(connectionLineFormat,
connectionStatus.getId(),
connectionStatus.getSourceName(),
connectionStatus.getName(),
connectionStatus.getDestinationName(),
input + inputDiff,
output + outputDiff,
queued + queuedDiff));
} else {
builder.append(String.format(connectionLineFormat,
connectionStatus.getId(),
connectionStatus.getSourceName(),
connectionStatus.getName(),
connectionStatus.getDestinationName(),
input,
output,
queued));
}
lastConnectionStatus.put(connectionStatus.getId(), connectionStatus);
}
}
private String toDiff(final long oldValue, final long newValue) {
return toDiff(oldValue, newValue, false, false);
}
private String toDiff(final long oldValue, final long newValue, final boolean formatDataSize, final boolean formatTime) {
if (formatDataSize && formatTime) {
throw new IllegalArgumentException("Cannot format units as both data size and time");
}
final long diff = Math.abs(newValue - oldValue);
final String formattedDiff = formatDataSize ? FormatUtils.formatDataSize(diff)
: (formatTime ? FormatUtils.formatHoursMinutesSeconds(diff, TimeUnit.NANOSECONDS) : String.valueOf(diff));
if (oldValue > newValue) {
return " (-" + formattedDiff + ")";
} else {
return " (+" + formattedDiff + ")";
}
}
private String toDiff(final long oldCount, final long oldBytes, final long newCount, final long newBytes) {
final long countDiff = Math.abs(newCount - oldCount);
final long bytesDiff = Math.abs(newBytes - oldBytes);
final StringBuilder sb = new StringBuilder();
sb.append(" (").append(oldCount > newCount ? "-" : "+").append(countDiff).append("/");
sb.append(oldBytes > newBytes ? "-" : "+");
sb.append(FormatUtils.formatDataSize(bytesDiff)).append(")");
return sb.toString();
}
// Recursively the status of all processors in this group.
private void printProcessorStatus(final ProcessGroupStatus groupStatus, final StringBuilder builder, final boolean showDeltas) {
final List<ProcessorStatus> processorStatuses = new ArrayList<>();
populateProcessorStatuses(groupStatus, processorStatuses);
Collections.sort(processorStatuses, new Comparator<ProcessorStatus>() {
@Override
public int compare(final ProcessorStatus o1, final ProcessorStatus o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return 1;
}
if (o2 == null) {
return -1;
}
return -Long.compare(o1.getProcessingNanos(), o2.getProcessingNanos());
}
});
for (final ProcessorStatus processorStatus : processorStatuses) {
// get the stats
final String input = processorStatus.getInputCount() + " / " + FormatUtils.formatDataSize(processorStatus.getInputBytes());
final String output = processorStatus.getOutputCount() + " / " + FormatUtils.formatDataSize(processorStatus.getOutputBytes());
final String read = FormatUtils.formatDataSize(processorStatus.getBytesRead());
final String written = FormatUtils.formatDataSize(processorStatus.getBytesWritten());
final String invocations = String.valueOf(processorStatus.getInvocations());
final long nanos = processorStatus.getProcessingNanos();
final String procTime = FormatUtils.formatHoursMinutesSeconds(nanos, TimeUnit.NANOSECONDS);
String runStatus = "";
if (processorStatus.getRunStatus() != null) {
runStatus = processorStatus.getRunStatus().toString();
}
final String inputDiff;
final String outputDiff;
final String readDiff;
final String writtenDiff;
final String invocationsDiff;
final String procTimeDiff;
final ProcessorStatus lastStatus = lastProcessorStatus.get(processorStatus.getId());
if (showDeltas && lastStatus != null) {
inputDiff = toDiff(lastStatus.getInputCount(), lastStatus.getInputBytes(), processorStatus.getInputCount(), processorStatus.getInputBytes());
outputDiff = toDiff(lastStatus.getOutputCount(), lastStatus.getOutputBytes(), processorStatus.getOutputCount(), processorStatus.getOutputBytes());
readDiff = toDiff(lastStatus.getBytesRead(), processorStatus.getBytesRead(), true, false);
writtenDiff = toDiff(lastStatus.getBytesWritten(), processorStatus.getBytesWritten(), true, false);
invocationsDiff = toDiff(lastStatus.getInvocations(), processorStatus.getInvocations());
procTimeDiff = toDiff(lastStatus.getProcessingNanos(), processorStatus.getProcessingNanos(), false, true);
} else {
inputDiff = toDiff(0L, 0L, processorStatus.getInputCount(), processorStatus.getInputBytes());
outputDiff = toDiff(0L, 0L, processorStatus.getOutputCount(), processorStatus.getOutputBytes());
readDiff = toDiff(0L, processorStatus.getBytesRead(), true, false);
writtenDiff = toDiff(0L, processorStatus.getBytesWritten(), true, false);
invocationsDiff = toDiff(0L, processorStatus.getInvocations());
procTimeDiff = toDiff(0L, processorStatus.getProcessingNanos(), false, true);
}
if (showDeltas) {
builder.append(String.format(processorLineFormat,
processorStatus.getName(),
processorStatus.getId(),
processorStatus.getType(),
runStatus,
input + inputDiff,
output + outputDiff,
read + readDiff,
written + writtenDiff,
invocations + invocationsDiff,
procTime + procTimeDiff));
} else {
builder.append(String.format(processorLineFormat,
processorStatus.getName(),
processorStatus.getId(),
processorStatus.getType(),
runStatus,
input,
output,
read,
written,
invocations,
procTime));
}
lastProcessorStatus.put(processorStatus.getId(), processorStatus);
}
}
}