package de.m3y.prometheus.exporter.fsimage; import java.io.FileNotFoundException; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.function.Function; import java.util.regex.Pattern; import de.m3y.hadoop.hdfs.hfsa.core.FSImageLoader; import de.m3y.hadoop.hdfs.hfsa.core.FsVisitor; import io.prometheus.client.Histogram; import io.prometheus.client.SimpleCollector; import io.prometheus.client.Summary; import org.apache.hadoop.fs.permission.PermissionStatus; import org.apache.hadoop.hdfs.server.namenode.FsImageProto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static de.m3y.prometheus.exporter.fsimage.FsImageCollector.METRIC_PREFIX; import static de.m3y.prometheus.exporter.fsimage.FsImageCollector.MetricFamilySamples; import static de.m3y.prometheus.exporter.fsimage.FsImageUpdateHandler.FSIZE; import static de.m3y.prometheus.exporter.fsimage.FsImageUpdateHandler.REPLICATION; /** * Generates a report for a loaded FSImage. */ public class FsImageReporter { private static final Logger LOG = LoggerFactory.getLogger(FsImageReporter.class); interface MetricAdapter { void observe(long metricValue); long count(); } static class HistogramMetricAdapter implements MetricAdapter { final Histogram.Child child; HistogramMetricAdapter(Histogram.Child child) { this.child = child; } @Override public void observe(long metricValue) { child.observe(metricValue); } @Override public long count() { return (long) sum(child.get().buckets); } private static double sum(double[] doubles) { double s = 0; for (double d : doubles) { s += d; } return s; } } static class SummaryMetricAdapter implements MetricAdapter { final Summary.Child child; SummaryMetricAdapter(Summary.Child child) { this.child = child; } @Override public void observe(long metricValue) { child.observe(metricValue); } @Override public long count() { return (long) child.get().count; } } abstract static class AbstractFileSystemStats { LongAdder sumDirectories = new LongAdder(); LongAdder sumBlocks = new LongAdder(); LongAdder sumSymLinks = new LongAdder(); final MetricAdapter fileSize; protected AbstractFileSystemStats(MetricAdapter fileSize) { this.fileSize = fileSize; } } static class OverallStats extends AbstractFileSystemStats { final Summary replication; OverallStats(MetricAdapter fileSize, Summary replication) { super(fileSize); this.replication = replication; } // No additional attributes, for now. } static class UserStats extends AbstractFileSystemStats { final String userName; final MetricAdapter replication; UserStats(String userName, MetricAdapter fileSize, MetricAdapter replication) { super(fileSize); this.userName = userName; this.replication = replication; } } static class GroupStats extends AbstractFileSystemStats { final String groupName; GroupStats(String groupName, MetricAdapter fileSize) { super(fileSize); this.groupName = groupName; } } static class PathStats extends AbstractFileSystemStats { final String path; PathStats(String path, MetricAdapter fileSize) { super(fileSize); this.path = path; } } /** * Contains collected statistics for an FSImage. */ static class Report { // Overall stats final OverallStats overallStats; final SimpleCollector overallFleSizeDistribution; final Summary overallReplication; // Group stats final Map<String, GroupStats> groupStats; final SimpleCollector groupFileSizeDistribution; final Function<String, GroupStats> createGroupStats; // User stats final Map<String, UserStats> userStats; final SimpleCollector userFileSizeDistribution; final Function<String, UserStats> createUserStat; final Summary userReplication; // Path stats final Map<String, PathStats> pathStats; final SimpleCollector<?> pathFileSizeDistribution; final Function<String, PathStats> createPathStat; boolean error = false; // Path sets final Map<String, PathStats> pathSetStats; final SimpleCollector pathSetFileSizeDistribution; final Function<String, PathStats> createPathSetStat; Report(Config config) { groupStats = new ConcurrentHashMap<>(); userStats = new ConcurrentHashMap<>(); pathStats = new ConcurrentHashMap<>(); pathSetStats = new ConcurrentHashMap<>(); double[] configuredBuckets = config.getFileSizeDistributionBucketsAsDoubles(); // Overall Histogram overallHistogram = Histogram.build() .name(METRIC_PREFIX + FSIZE) .buckets(configuredBuckets) .help("Overall file size distribution") .create(); overallFleSizeDistribution = overallHistogram; overallReplication = Summary.build() .name(FsImageCollector.METRIC_PREFIX + REPLICATION) .help("Overall file replication").create(); overallStats = new OverallStats(new HistogramMetricAdapter(overallHistogram.labels()), overallReplication); // Group if (config.isSkipFileDistributionForGroupStats()) { Summary summary = Summary.build() .name(FsImageUpdateHandler.METRIC_PREFIX_GROUP + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_GROUP_NAME) .help("Per group file size and file count").create(); createGroupStats = groupName -> new GroupStats(groupName, new SummaryMetricAdapter(summary.labels(groupName))); groupFileSizeDistribution = summary; } else { Histogram histogram = Histogram.build() .name(FsImageUpdateHandler.METRIC_PREFIX_GROUP + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_GROUP_NAME) .buckets(configuredBuckets) .help("Per group file size distribution.").create(); createGroupStats = groupName -> new GroupStats(groupName, new HistogramMetricAdapter(histogram.labels(groupName))); groupFileSizeDistribution = histogram; } // User userReplication = Summary.build() .name(FsImageUpdateHandler.METRIC_PREFIX_USER + REPLICATION) .labelNames(FsImageUpdateHandler.LABEL_USER_NAME) .help("Per user file replication").create(); if (config.isSkipFileDistributionForUserStats()) { Summary summary = Summary.build() .name(FsImageUpdateHandler.METRIC_PREFIX_USER + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_USER_NAME) .help("Per user file size and file count").create(); createUserStat = userName -> new UserStats(userName, new SummaryMetricAdapter(summary.labels(userName)), new SummaryMetricAdapter(userReplication.labels(userName))); userFileSizeDistribution = summary; } else { Histogram histogram = Histogram.build() .name(FsImageUpdateHandler.METRIC_PREFIX_USER + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_USER_NAME) .buckets(configuredBuckets) .help("Per user file size distribution").create(); createUserStat = userName -> new UserStats(userName, new HistogramMetricAdapter(histogram.labels(userName)), new SummaryMetricAdapter(userReplication.labels(userName))); userFileSizeDistribution = histogram; } // Paths if (config.isSkipFileDistributionForPathStats()) { Summary summary = Summary.build() .name(FsImageUpdateHandler.METRIC_PREFIX_PATH + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_PATH) .help("Path specific file size and file count").create(); createPathStat = path -> new PathStats(path, new SummaryMetricAdapter(summary.labels(path))); pathFileSizeDistribution = summary; } else { Histogram histogram = Histogram.build() .name(FsImageUpdateHandler.METRIC_PREFIX_PATH + FSIZE) .buckets(configuredBuckets) .labelNames(FsImageUpdateHandler.LABEL_PATH) .help("Path specific file size distribution").create(); createPathStat = path -> new PathStats(path, new HistogramMetricAdapter(histogram.labels(path))); pathFileSizeDistribution = histogram; } // Path sets if (config.isSkipFileDistributionForPathSetStats()) { Summary summary = Summary.build() .name(FsImageUpdateHandler.METRIC_PREFIX_PATH_SET + FSIZE) .labelNames(FsImageUpdateHandler.LABEL_PATH_SET) .help("Path set specific file size and file count").create(); createPathSetStat = path -> new PathStats(path, new SummaryMetricAdapter(summary.labels(path))); pathSetFileSizeDistribution = summary; } else { Histogram histogram = Histogram.build() .name(FsImageUpdateHandler.METRIC_PREFIX_PATH_SET + FSIZE) .buckets(configuredBuckets) .labelNames(FsImageUpdateHandler.LABEL_PATH_SET) .help("Path set specific file size distribution").create(); createPathSetStat = path -> new PathStats(path, new HistogramMetricAdapter(histogram.labels(path))); pathSetFileSizeDistribution = histogram; } } public void collect(List<MetricFamilySamples> mfs) { mfs.addAll(overallFleSizeDistribution.collect()); mfs.addAll(overallReplication.collect()); mfs.addAll(groupFileSizeDistribution.collect()); mfs.addAll(userFileSizeDistribution.collect()); mfs.addAll(userReplication.collect()); if (hasPathStats()) { mfs.addAll(pathFileSizeDistribution.collect()); } if (hasPathSetStats()) { mfs.addAll(pathSetFileSizeDistribution.collect()); } } boolean hasPathStats() { return null != pathStats && !pathStats.isEmpty(); } boolean hasPathSetStats() { return null != pathSetStats && !pathSetStats.isEmpty(); } } private FsImageReporter() { // Nothing } static Report computeStatsReport(final FSImageLoader loader, Config config) throws IOException { Report report = new Report(config); final OverallStats overallStats = report.overallStats; long t = System.currentTimeMillis(); loader.visitParallel(new FsVisitor() { @Override public void onFile(FsImageProto.INodeSection.INode inode, String path) { FsImageProto.INodeSection.INodeFile f = inode.getFile(); PermissionStatus p = loader.getPermissionStatus(f.getPermission()); final long fileSize = FSImageLoader.getFileSize(f); final long fileBlocks = f.getBlocksCount(); overallStats.sumBlocks.add(fileBlocks); overallStats.fileSize.observe(fileSize); overallStats.replication.observe(f.getReplication()); // Group stats final String groupName = p.getGroupName(); final GroupStats groupStat = report.groupStats.computeIfAbsent(groupName, report.createGroupStats); groupStat.sumBlocks.add(fileBlocks); groupStat.fileSize.observe(fileSize); // User stats final String userName = p.getUserName(); UserStats userStat = report.userStats.computeIfAbsent(userName, report.createUserStat); userStat.sumBlocks.add(fileBlocks); userStat.fileSize.observe(fileSize); userStat.replication.observe(f.getReplication()); } @Override public void onDirectory(FsImageProto.INodeSection.INode inode, String path) { FsImageProto.INodeSection.INodeDirectory d = inode.getDirectory(); PermissionStatus p = loader.getPermissionStatus(d.getPermission()); if (LOG.isDebugEnabled()) { LOG.debug("Visiting directory {}", path + ("/".equals(path) ? "" : "/") + inode.getName().toStringUtf8()); } // Group stats final String groupName = p.getGroupName(); final GroupStats groupStat = report.groupStats.computeIfAbsent(groupName, report.createGroupStats); groupStat.sumDirectories.increment(); // User stats final String userName = p.getUserName(); final UserStats userStat = report.userStats.computeIfAbsent(userName, report.createUserStat); userStat.sumDirectories.increment(); overallStats.sumDirectories.increment(); } @Override public void onSymLink(FsImageProto.INodeSection.INode inode, String path) { FsImageProto.INodeSection.INodeSymlink d = inode.getSymlink(); PermissionStatus p = loader.getPermissionStatus(d.getPermission()); // Group stats final String groupName = p.getGroupName(); final GroupStats groupStat = report.groupStats.computeIfAbsent(groupName, report.createGroupStats); groupStat.sumSymLinks.increment(); // User stats final String userName = p.getUserName(); final UserStats userStat = report.userStats.computeIfAbsent(userName, report.createUserStat); userStat.sumSymLinks.increment(); overallStats.sumSymLinks.increment(); } }); LOG.info("Finished computing overall/group/user stats in {}ms", System.currentTimeMillis() - t); if (config.hasPaths()) { computePathStats(loader, config, report); } if (config.hasPathSets()) { computePathSetStatsParallel(loader, config, report); } return report; } private static void computePathStats(FSImageLoader loader, Config config, Report report) throws IOException { Set<String> expandedPaths = expandPaths(loader, config.getPaths()); LOG.info("Expanded paths {} for path stats {}", expandedPaths, config.getPaths()); long s = System.currentTimeMillis(); expandedPaths.parallelStream().forEach(p -> { try { long t = System.currentTimeMillis(); final PathStats pathStats = report.pathStats.computeIfAbsent(p, report.createPathStat); loader.visit(new PathStatVisitor(pathStats), p); // Subtract start dir, as only child dirs count pathStats.sumDirectories.decrement(); if (LOG.isDebugEnabled()) { LOG.debug("Finished path stat for {} with {} total number of files in {}ms", p, pathStats.fileSize.count(), System.currentTimeMillis() - t); } } catch (IOException e) { LOG.error("Can not traverse " + p, e); report.error = true; } }); LOG.info("Finished {} path stats in {}ms", report.pathStats.size(), System.currentTimeMillis() - s); } private static void computePathSetStatsParallel(FSImageLoader loader, Config config, Report report) { long s = System.currentTimeMillis(); config.getPathSets().entrySet().parallelStream().forEach(entry -> computePathSetStats(loader, entry, report) ); LOG.info("Finished {} path set stats in {}ms", report.pathSetStats.size(), System.currentTimeMillis() - s); } private static void computePathSetStats(FSImageLoader loader, Map.Entry<String, List<String>> entry, Report report) { try { Set<String> expandedPaths = expandPaths(loader, entry.getValue()); LOG.info("Expanded paths {} for path set stats {}", expandedPaths, entry.getKey()); long t = System.currentTimeMillis(); final PathStats pathStats = report.pathSetStats.computeIfAbsent(entry.getKey(), report.createPathSetStat); final PathStatVisitor visitor = new PathStatVisitor(pathStats); for (String path : expandedPaths) { loader.visit(visitor, path); } // Subtract number of start dirs, as only child dirs count pathStats.sumDirectories.add(-expandedPaths.size()); if (LOG.isDebugEnabled()) { LOG.debug("Finished path set stat for {} with {} total number of files in {}ms", entry.getKey(), pathStats.fileSize.count(), System.currentTimeMillis() - t); } } catch (IOException e) { LOG.error("Can not traverse path set " + entry.getKey() + " using paths " + entry.getValue(), e); report.error = true; } } static Set<String> expandPaths(FSImageLoader loader, Collection<String> paths) throws IOException { Set<String> expandedPaths = new HashSet<>(); for (String path : paths) { // If path does not exist, match child directories if (!hasDirectory(loader, path)) { addMatchingPaths(loader, expandedPaths, path); } else { // Existing directory expandedPaths.add(path); } } return expandedPaths; } private static void addMatchingPaths(FSImageLoader loader, Set<String> expandedPaths, String path) throws IOException { int idx = path.lastIndexOf('/'); if (idx < 0) { LOG.error("Skipping invalid path <{}> for per-path-stats", path); } else { String parent = (idx == 0 ? "/" : path.substring(0, idx)); try { List<String> childPaths = loader.getChildPaths(parent); Pattern pattern = Pattern.compile(path); childPaths.removeIf(p -> !pattern.matcher(p).matches()); if (childPaths.isEmpty()) { LOG.warn("No matches found for {}", path); } else { expandedPaths.addAll(childPaths); } } catch(FileNotFoundException|NoSuchElementException ex) { LOG.warn("Skipping configured, non-existing path {} for metric computations." + " Check your configuration path/pathSet entries!", parent); } } } /** * TODO: Replace once FSImageLoader contains this functionality */ private static boolean hasDirectory(FSImageLoader loader, String path) throws IOException { if ("/".equals(path)) { // Root always exists return true; } try { return null != loader.getINodeFromPath(path); } catch (FileNotFoundException | IllegalArgumentException ex) { return false; } } static class PathStatVisitor implements FsVisitor { private final PathStats pathStats; PathStatVisitor(PathStats pathStats) { this.pathStats = pathStats; } @Override public void onFile(FsImageProto.INodeSection.INode inode, String path) { FsImageProto.INodeSection.INodeFile f = inode.getFile(); pathStats.sumBlocks.add(f.getBlocksCount()); final long fileSize = FSImageLoader.getFileSize(f); pathStats.fileSize.observe(fileSize); } @Override public void onDirectory(FsImageProto.INodeSection.INode inode, String path) { pathStats.sumDirectories.increment(); } @Override public void onSymLink(FsImageProto.INodeSection.INode inode, String path) { pathStats.sumSymLinks.increment(); } } }