/* * 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 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.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.Validator; import org.apache.nifi.processor.DataUnit; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.reporting.AbstractReportingTask; import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.reporting.ReportingContext; import org.apache.nifi.util.FormatUtils; import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryUsage; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** * Reporting task used to monitor usage of memory after Garbage Collection has * been performed. * * Each time this ReportingTask runs, it checks the amount of memory that was * used in the configured Memory Pool after the last garbage collection took * place. If the amount of memory used exceeds a configurable threshold, a * System-Level Bulletin is generated and a message is logged. If the memory * usage did not exceed the configured threshold but the previous iteration did, * an INFO level message is logged and an associated bulletin published. * * The following properties are supported: * * <ul> * <li><b>Memory Pool</b> - The name of the JVM Memory Pool. The allowed values * depend on the JVM and operating system. * * <p> * <br />The following values are typically supported: * <ul> * <li>PS Old Gen</li> * <li>PS Survivor Space</li> * <li>PS Eden Space</li> * <li>PS Perm Gen</li> * </ul> * </li> * <li> * <b>Usage Threshold</b> - The threshold for memory usage that will cause a * notification to occur. The format can either be a percentage (e.g., 80%) or a * Data Size (e.g., 1 GB) * </li> * <li> * <b>Reporting Interval</b> - How often a notification should occur in the * event that the memory usage exceeds the threshold. This differs from the * scheduling period such that having a short value for the scheduling period * and a long value for the reportingInterval property will result in checking * the memory usage often so that notifications happen quickly but prevents * notifications from continually being generated. The format of this property * is The Period format (e.g., 5 mins). * </li> * </ul> */ @Tags({"monitor", "memory", "heap", "jvm", "gc", "garbage collection", "warning"}) @CapabilityDescription("Checks the amount of Java Heap available in the JVM for a particular JVM Memory Pool. If the" + " amount of space used exceeds some configurable threshold, will warn (via a log message and System-Level Bulletin)" + " that the memory pool is exceeding this threshold.") public class MonitorMemory extends AbstractReportingTask { private static final AllowableValue[] memPoolAllowableValues; static { List<MemoryPoolMXBean> memoryPoolBeans = ManagementFactory.getMemoryPoolMXBeans(); memPoolAllowableValues = new AllowableValue[memoryPoolBeans.size()]; for (int i = 0; i < memPoolAllowableValues.length; i++) { memPoolAllowableValues[i] = new AllowableValue(memoryPoolBeans.get(i).getName()); } } public static final PropertyDescriptor MEMORY_POOL_PROPERTY = new PropertyDescriptor.Builder() .name("Memory Pool") .displayName("Memory Pool") .description("The name of the JVM Memory Pool to monitor") .required(true) .allowableValues(memPoolAllowableValues) .build(); public static final PropertyDescriptor THRESHOLD_PROPERTY = new PropertyDescriptor.Builder() .name("Usage Threshold") .displayName("Usage Threshold") .description("Indicates the threshold at which warnings should be generated") .required(true) .addValidator(new ThresholdValidator()) .defaultValue("65%") .build(); public static final PropertyDescriptor REPORTING_INTERVAL = new PropertyDescriptor.Builder() .name("Reporting Interval") .displayName("Reporting Interval") .description("Indicates how often this reporting task should report bulletins while the memory utilization exceeds the configured threshold") .required(false) .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) .defaultValue(null) .build(); public static final Pattern PERCENTAGE_PATTERN = Pattern.compile("\\d{1,2}%"); public static final Pattern DATA_SIZE_PATTERN = DataUnit.DATA_SIZE_PATTERN; public static final Pattern TIME_PERIOD_PATTERN = FormatUtils.TIME_DURATION_PATTERN; private volatile MemoryPoolMXBean monitoredBean; private volatile String threshold = "65%"; private volatile long lastReportTime; private volatile long reportingIntervalMillis; private volatile boolean lastValueWasExceeded; private final static List<PropertyDescriptor> propertyDescriptors; static { List<PropertyDescriptor> _propertyDescriptors = new ArrayList<>(); _propertyDescriptors.add(MEMORY_POOL_PROPERTY); _propertyDescriptors.add(THRESHOLD_PROPERTY); _propertyDescriptors.add(REPORTING_INTERVAL); propertyDescriptors = Collections.unmodifiableList(_propertyDescriptors); } @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return propertyDescriptors; } @OnScheduled public void onConfigured(final ConfigurationContext config) throws InitializationException { final String desiredMemoryPoolName = config.getProperty(MEMORY_POOL_PROPERTY).getValue(); final String thresholdValue = config.getProperty(THRESHOLD_PROPERTY).getValue().trim(); threshold = thresholdValue; final Long reportingIntervalValue = config.getProperty(REPORTING_INTERVAL).asTimePeriod(TimeUnit.MILLISECONDS); if (reportingIntervalValue == null) { reportingIntervalMillis = config.getSchedulingPeriod(TimeUnit.MILLISECONDS); } else { reportingIntervalMillis = reportingIntervalValue; } final List<MemoryPoolMXBean> memoryPoolBeans = ManagementFactory.getMemoryPoolMXBeans(); for (int i = 0; i < memoryPoolBeans.size() && monitoredBean == null; i++) { MemoryPoolMXBean memoryPoolBean = memoryPoolBeans.get(i); String memoryPoolName = memoryPoolBean.getName(); if (desiredMemoryPoolName.equals(memoryPoolName)) { monitoredBean = memoryPoolBean; if (memoryPoolBean.isCollectionUsageThresholdSupported()) { long calculatedThreshold; if (DATA_SIZE_PATTERN.matcher(thresholdValue).matches()) { calculatedThreshold = DataUnit.parseDataSize(thresholdValue, DataUnit.B).longValue(); } else { final String percentage = thresholdValue.substring(0, thresholdValue.length() - 1); final double pct = Double.parseDouble(percentage) / 100D; calculatedThreshold = (long) (monitoredBean.getUsage().getMax() * pct); } monitoredBean.setUsageThreshold(calculatedThreshold); } } } if (monitoredBean == null) { throw new InitializationException("Found no JVM Memory Pool with name " + desiredMemoryPoolName + "; will not monitor Memory Pool"); } } @Override public void onTrigger(final ReportingContext context) { final MemoryPoolMXBean bean = monitoredBean; if (bean == null) { return; } final MemoryUsage usage = bean.getUsage(); if (usage == null) { getLogger().warn("{} could not determine memory usage for pool with name {}", new Object[] {this, context.getProperty(MEMORY_POOL_PROPERTY)}); return; } final double percentageUsed = (double) usage.getUsed() / (double) usage.getMax() * 100D; if (bean.isUsageThresholdExceeded()) { if (System.currentTimeMillis() < reportingIntervalMillis + lastReportTime && lastReportTime > 0L) { return; } lastReportTime = System.currentTimeMillis(); lastValueWasExceeded = true; final String message = String.format("Memory Pool '%1$s' has exceeded the configured Threshold of %2$s, having used %3$s / %4$s (%5$.2f%%)", bean.getName(), threshold, FormatUtils.formatDataSize(usage.getUsed()), FormatUtils.formatDataSize(usage.getMax()), percentageUsed); getLogger().warn("{}", new Object[] {message}); } else if (lastValueWasExceeded) { lastValueWasExceeded = false; lastReportTime = System.currentTimeMillis(); final String message = String.format("Memory Pool '%1$s' is no longer exceeding the configured Threshold of %2$s; currently using %3$s / %4$s (%5$.2f%%)", bean.getName(), threshold, FormatUtils.formatDataSize(usage.getUsed()), FormatUtils.formatDataSize(usage.getMax()), percentageUsed); getLogger().info("{}", new Object[] {message}); } } private static class ThresholdValidator implements Validator { @Override public ValidationResult validate(final String subject, final String input, final ValidationContext context) { if (!PERCENTAGE_PATTERN.matcher(input).matches() && !DATA_SIZE_PATTERN.matcher(input).matches()) { return new ValidationResult.Builder().input(input).subject(subject).valid(false) .explanation("Valid value is a number in the range of 0-99 followed by a percent sign (e.g. 65%) or a Data Size (e.g. 100 MB)").build(); } return new ValidationResult.Builder().input(input).subject(subject).valid(true).build(); } } }