diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/ReliableTaildirEventReader.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/ReliableTaildirEventReader.java index ae9583620a..264a0c6c0f 100644 --- a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/ReliableTaildirEventReader.java +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/ReliableTaildirEventReader.java @@ -49,7 +49,7 @@ public class ReliableTaildirEventReader implements ReliableEventReader { private static final Logger logger = LoggerFactory.getLogger(ReliableTaildirEventReader.class); - private final List taildirCache; + private final List taildirCache; private final Table headerTable; private TailFile currentFile = null; @@ -65,22 +65,34 @@ public class ReliableTaildirEventReader implements ReliableEventReader { * Create a ReliableTaildirEventReader to watch the given directory. */ private ReliableTaildirEventReader(Map filePaths, + Map filePathsIncludeChild, Table headerTable, String positionFilePath, boolean skipToEnd, boolean addByteOffset, boolean cachePatternMatching, boolean annotateFileName, String fileNameHeader) throws IOException { // Sanity checks - Preconditions.checkNotNull(filePaths); + if (filePaths == null && filePathsIncludeChild == null) { + throw new NullPointerException(); + } Preconditions.checkNotNull(positionFilePath); if (logger.isDebugEnabled()) { logger.debug("Initializing {} with directory={}", - new Object[] { ReliableTaildirEventReader.class.getSimpleName(), filePaths }); + new Object[]{ReliableTaildirEventReader.class.getSimpleName(), filePaths}); } - List taildirCache = Lists.newArrayList(); - for (Entry e : filePaths.entrySet()) { - taildirCache.add(new TaildirMatcher(e.getKey(), e.getValue(), cachePatternMatching)); + List taildirCache = Lists.newArrayList(); + if (filePaths != null) { + for (Entry e : filePaths.entrySet()) { + taildirCache.add(new TaildirMatcher(e.getKey(), e.getValue(), cachePatternMatching)); + } + } + if (filePathsIncludeChild != null) { + for (Map.Entry e : filePathsIncludeChild.entrySet()) { + taildirCache.add( + new TaildirIncludeChildMatcher(e.getKey(), e.getValue(), cachePatternMatching)); + } } + logger.info("taildirCache: " + taildirCache.toString()); logger.info("headerTable: " + headerTable.toString()); @@ -187,7 +199,7 @@ public List readEvents(int numEvents, boolean backoffWithoutNL) throws IOException { if (!committed) { if (currentFile == null) { - throw new IllegalStateException("current file does not exist. " + currentFile.getPath()); + throw new IllegalStateException("current file does not exist. "); } logger.info("Last read was never committed - resetting position"); long lastPos = currentFile.getPos(); @@ -239,7 +251,7 @@ public List updateTailFiles(boolean skipToEnd) throws IOException { updateTime = System.currentTimeMillis(); List updatedInodes = Lists.newArrayList(); - for (TaildirMatcher taildir : taildirCache) { + for (TailMatcher taildir : taildirCache) { Map headers = headerTable.row(taildir.getFileGroup()); for (File f : taildir.getMatchingFiles()) { @@ -247,6 +259,7 @@ public List updateTailFiles(boolean skipToEnd) throws IOException { try { inode = getInode(f); } catch (NoSuchFileException e) { + taildir.deleteFileCache(f); logger.info("File has been deleted in the meantime: " + e.getMessage()); continue; } @@ -299,6 +312,7 @@ private TailFile openFile(File file, Map headers, long inode, lo */ public static class Builder { private Map filePaths; + private Map filePathsIncludeChild; private Table headerTable; private String positionFilePath; private boolean skipToEnd; @@ -314,6 +328,11 @@ public Builder filePaths(Map filePaths) { return this; } + public Builder filePathsIncludeChild(Map filePathsIncludeChild) { + this.filePathsIncludeChild = filePathsIncludeChild; + return this; + } + public Builder headerTable(Table headerTable) { this.headerTable = headerTable; return this; @@ -350,7 +369,8 @@ public Builder fileNameHeader(String fileNameHeader) { } public ReliableTaildirEventReader build() throws IOException { - return new ReliableTaildirEventReader(filePaths, headerTable, positionFilePath, skipToEnd, + return new ReliableTaildirEventReader(filePaths, filePathsIncludeChild, headerTable, + positionFilePath, skipToEnd, addByteOffset, cachePatternMatching, annotateFileName, fileNameHeader); } diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TailMatcher.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TailMatcher.java new file mode 100644 index 0000000000..2ab74f81a3 --- /dev/null +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TailMatcher.java @@ -0,0 +1,34 @@ +/* + * 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.source.taildir; + +import java.io.File; +import java.util.List; + +/** + * Identifies and caches the files matched + */ +public interface TailMatcher { + List getMatchingFiles(); + + String getFileGroup(); + + void deleteFileCache(File file); +} diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirIncludeChildMatcher.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirIncludeChildMatcher.java new file mode 100644 index 0000000000..92578f55f4 --- /dev/null +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirIncludeChildMatcher.java @@ -0,0 +1,382 @@ +/* + * 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.source.taildir; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.apache.flume.annotations.InterfaceAudience; +import org.apache.flume.annotations.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.DirectoryStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.Collections; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +/** + * Identifies and caches the files matched by single file pattern for {@code TAILDIR} source. + *

+ * file patterns apply to the fileNames and files in subdirectories, + * implementation checks the parent directory and subdirectories for modification + * (additional or removed files update modification time of dir) + * If no modification happened to the dir that means the underlying files could only be + * written to but no need to rerun the pattern matching on fileNames. + * If a directory has modified, can only re-match the directory, no need to match other directories + *

+ * This implementation provides lazy caching or no caching. Instances of this class keep the + * result file list from the last successful execution of {@linkplain #getMatchingFiles()} + * function invocation, and may serve the content without hitting the FileSystem for performance + * optimization. + *

+ * IMPORTANT: It is assumed that the hosting system provides at least second granularity + * for both {@code System.currentTimeMillis()} and {@code File.lastModified()}. Also + * that system clock is used for file system timestamps. If it is not the case then configure it + * as uncached. Class is solely for package only usage. Member functions are not thread safe. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class TaildirIncludeChildMatcher implements TailMatcher { + private static final Logger logger = LoggerFactory.getLogger(TaildirIncludeChildMatcher.class); + + // flag from configuration to switch off caching completely + private final boolean cachePatternMatching; + // id from configuration + private final String fileGroup; + // plain string of the desired files from configuration + private final String filePattern; + + // directory monitored for changes + private Set parentDirList = Sets.newLinkedHashSet(); + + // Key is file path + // Value is a two tuple, contains the lastSeenParentDirMTime and lastCheckedTime of the file + private Map lastTimeMap = Maps.newHashMap(); + + // cached content, files which matched the pattern within the parent directory + private Set lastMatchedFiles = Sets.newHashSet(); + + // Array version cache of lastMatchedFilesSet, use this cache when the files has not been changed + private List lastMatchedFilesCache = new ArrayList<>(); + + // file regex + private final String regex; + + TaildirIncludeChildMatcher(String fileGroup, String filePattern, boolean cachePatternMatching) { + // store whatever came from configuration + this.fileGroup = fileGroup; + this.filePattern = filePattern; + this.cachePatternMatching = cachePatternMatching; + + // Path to the root directory to be monitored + // The end of the path in the configuration file can be filled + // with the file regular that needs to be matched + // Note that if "/" is not written at the end of the path, + // the end field will be treated as a regular + String filePatternParent; + + if (filePattern.charAt(filePattern.length() - 1) == '/') { + filePatternParent = filePattern; + regex = ""; + } else { + String[] res = filePattern.split("\\/"); + List list = new ArrayList<>(Arrays.asList(res)); + regex = list.remove(list.size() - 1); + filePatternParent = StringUtils.join(list, "/"); + } + + File f = new File(filePatternParent); + + // Scan from the top directory + // Scan out all subdirectories and put them into cache + getFileGroupChild(f, this.parentDirList); + + Preconditions.checkState(f.exists(), + "Directory does not exist: " + f.getAbsolutePath()); + } + + /** + * Lists those files within the parentDir + * and subdirectory that match regex pattern passed in during object + * instantiation. Designed for frequent periodic invocation + * {@link org.apache.flume.source.PollableSourceRunner}. + *

+ * Based on the modification of the parentDirList(parentDir and its subdirectories) + * this function may trigger cache recalculation by + * calling {@linkplain #updateMatchingFilesNoCache(File, List)} or + * return the value stored in {@linkplain #lastMatchedFilesCache}. + * Parentdir is allowed to be a symbolic link. + *

+ * Files returned by this call are weakly consistent (see {@link DirectoryStream}). + * It does not freeze the directory while iterating, + * so it may (or may not) reflect updates to the directory that occur during the call, + * In which case next call + * will return those files (as mtime is increasing it won't hit cache but trigger recalculation). + * It is guaranteed that invocation reflects every change which was observable at the time of + * invocation. + *

+ * Matching file list recalculation is triggered when caching was turned off or + * if mtime is greater than the previously seen mtime + * (including the case of cache hasn't been calculated before). + * Additionally if a constantly updated directory was configured + * as parentDir and its subdirectories + * then multiple changes to the parentDirList may happen + * within the same second so in such case (assuming at least second granularity of reported mtime) + * it is impossible to tell whether a change of the dir happened before the check or after + * (unless the check happened after that second). + * Having said that implementation also stores system time of the previous invocation and previous + * invocation has to happen strictly after the current mtime to avoid further cache refresh + * (because then it is guaranteed that previous invocation resulted in valid cache content). + * If system clock hasn't passed the second of + * the current mtime then logic expects more changes as well + * (since it cannot be sure that there won't be any further changes still in that second + * and it would like to avoid data loss in first place) + * hence it recalculates matching files. If system clock finally + * passed actual mtime then a subsequent invocation guarantees that it picked up every + * change from the passed second so + * any further invocations can be served from cache associated with that second + * (given mtime is not updated again). + *

+ * Only the changed directories and new subdirectories will be rescanned each time + * + * @return List of files matching the pattern sorted by last modification time. No recursion. + * No directories. If nothing matches then returns an empty list. If I/O issue occurred then + * returns the list collected to the point when exception was thrown. + */ + @Override + public List getMatchingFiles() { + boolean lastMatchedFilesHasChange = false; + + long now = TimeUnit.SECONDS.toMillis( + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); + + List nonExistDirList = new ArrayList<>(); + + List newDirList = new ArrayList<>(); + + // Traverse all monitored directories + // if any directory changes are found, recalculate the file list of the directory + for (File dir : this.parentDirList) { + long currentParentDirMTime = dir.lastModified(); + + if (currentParentDirMTime == 0) { + this.lastTimeMap.remove(dir.getPath()); + nonExistDirList.add(dir); + continue; + } + + LastTimeTuple lastTimeTuple = lastTimeMap.get(dir.getPath()); + if (lastTimeTuple == null) { + lastTimeTuple = new LastTimeTuple(); + lastTimeMap.put(dir.getPath(), lastTimeTuple); + } + + Long lastSeenParentDirMTime = lastTimeTuple.getLastSeenParentDirMTime(); + Long lastCheckedTime = lastTimeTuple.getLastCheckedTime(); + + // calculate matched files if + // - we don't want to use cache (recalculate every time) OR + // - directory was clearly updated after the last check OR + // - last mtime change wasn't already checked for sure + // (system clock hasn't passed that second yet) + if (!cachePatternMatching || + lastSeenParentDirMTime < currentParentDirMTime || + lastCheckedTime < currentParentDirMTime) { + lastMatchedFilesHasChange = true; + + updateMatchingFilesNoCache(dir, newDirList); + lastTimeTuple.setLastSeenParentDirMTime(currentParentDirMTime); + lastTimeTuple.setLastCheckedTime(now); + } + } + + if (!nonExistDirList.isEmpty()) { + this.parentDirList.removeAll(nonExistDirList); + } + + if (!newDirList.isEmpty()) { + this.parentDirList.addAll(newDirList); + } + + if (lastMatchedFilesHasChange) { + this.lastMatchedFilesCache = sortByLastModifiedTime(new ArrayList<>(this.lastMatchedFiles)); + } + + return this.lastMatchedFilesCache; + } + + /** + * Provides the actual files within the parentDir which + * files are matching the regex pattern. Each invocation uses {@link Pattern} + * to identify matching files. + *

+ * Files returned by this call are weakly consistent + * (new files will be set {@linkplain #lastMatchedFilesCache}). + * It does not freeze the directory while iterating, so it may (or may not) reflect updates + * to the directory that occur during the call. In which case next call will return those files. + *

+ * New dir will be stored in the parameters newDirList + * New file will be stored in the {@linkplain #lastMatchedFiles} + * + * @param dir Directory to be scanned + * @param newDirList Used to store the new directory + */ + private void updateMatchingFilesNoCache(File dir, List newDirList) { + try (DirectoryStream stream = Files.newDirectoryStream(dir.toPath())) { + if (stream != null) { + for (Path child : stream) { + if (Files.isDirectory(child)) { + File newDir = child.toFile(); + if (!this.parentDirList.contains(newDir)) { + newDirList.add(newDir); + updateMatchingFilesNoCache(newDir, newDirList); + } + } else { + if (child.toString().matches(regex) || regex == "") { + this.lastMatchedFiles.add(child.toFile()); + } + } + } + } + } catch (IOException e) { + logger.error("I/O exception occurred while listing parent directory. " + + "Files already matched will be returned. " + dir.toPath(), e); + } + } + + /** + * Scan all subdirectories of the specified directory and cache + * + * @param fileGroup Directory to be scanned + * @param fileGroupList Store the parent directory and its subdirectories + * @return void + */ + private static void getFileGroupChild(File fileGroup, Set fileGroupList) { + fileGroupList.add(fileGroup); + + File[] listFiles = fileGroup.listFiles(); + if (listFiles != null) { + for (File child : listFiles) { + if (Files.isDirectory(child.toPath())) { + getFileGroupChild(child, fileGroupList); + } + } + } + } + + /** + * Utility function to sort matched files based on last modification time. + * Sorting itself use only a snapshot of last modification times captured before the sorting + * to keep the number of stat system calls to the required minimum. + * + * @param files list of files in any order + * @return sorted list + */ + private static List sortByLastModifiedTime(List files) { + final HashMap lastModificationTimes = new HashMap(files.size()); + for (File f : files) { + lastModificationTimes.put(f, f.lastModified()); + } + Collections.sort(files, new Comparator() { + @Override + public int compare(File o1, File o2) { + return lastModificationTimes.get(o1).compareTo(lastModificationTimes.get(o2)); + } + }); + + return files; + } + + @Override + public String toString() { + return "{" + + "filegroup='" + fileGroup + '\'' + + ", filePattern='" + filePattern + '\'' + + ", cached=" + cachePatternMatching + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TaildirIncludeChildMatcher that = (TaildirIncludeChildMatcher) o; + + return fileGroup.equals(that.fileGroup); + } + + @Override + public int hashCode() { + return fileGroup.hashCode(); + } + + @Override + public String getFileGroup() { + return fileGroup; + } + + @Override + public void deleteFileCache(File file) { + this.lastMatchedFiles.remove(file); + } + + private static class LastTimeTuple { + // system time in milliseconds, stores the last modification time of the + // parent directory seen by the last check, rounded to seconds + // initial value is used in first check only when it will be replaced instantly + // (system time is positive) + private long lastSeenParentDirMTime = -1; + + // system time in milliseconds, time of the last check, rounded to seconds + // initial value is used in first check only when it will be replaced instantly + // (system time is positive) + private long lastCheckedTime = -1; + + public long getLastCheckedTime() { + return lastCheckedTime; + } + + public long getLastSeenParentDirMTime() { + return lastSeenParentDirMTime; + } + + public void setLastCheckedTime(long lastCheckedTime) { + this.lastCheckedTime = lastCheckedTime; + } + + public void setLastSeenParentDirMTime(long lastSeenParentDirMTime) { + this.lastSeenParentDirMTime = lastSeenParentDirMTime; + } + } +} diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirMatcher.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirMatcher.java index ad9f720170..d7820dbd3e 100644 --- a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirMatcher.java +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirMatcher.java @@ -65,7 +65,7 @@ */ @InterfaceAudience.Private @InterfaceStability.Evolving -public class TaildirMatcher { +public class TaildirMatcher implements TailMatcher { private static final Logger logger = LoggerFactory.getLogger(TaildirMatcher.class); private static final FileSystem FS = FileSystems.getDefault(); @@ -180,11 +180,11 @@ public boolean accept(Path entry) throws IOException { * * @see #getMatchingFilesNoCache() */ - List getMatchingFiles() { + @Override + public List getMatchingFiles() { long now = TimeUnit.SECONDS.toMillis( TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); long currentParentDirMTime = parentDir.lastModified(); - List result; // calculate matched files if // - we don't want to use cache (recalculate every time) OR @@ -279,8 +279,17 @@ public int hashCode() { return fileGroup.hashCode(); } + @Override public String getFileGroup() { return fileGroup; } + // This method is used to delete the cache of nonexistent files. + // This matcher object is updated all caches will be automatically refreshed, + // so there is no need to delete them + @Override + public void deleteFileCache(File file) { + + } + } diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSource.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSource.java index 9ecccd7487..cedacbba98 100644 --- a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSource.java +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSource.java @@ -63,6 +63,7 @@ public class TaildirSource extends AbstractSource implements private static final Logger logger = LoggerFactory.getLogger(TaildirSource.class); private Map filePaths; + private Map filePathsIncludeChild; private Table headerTable; private int batchSize; private String positionFilePath; @@ -95,6 +96,7 @@ public synchronized void start() { try { reader = new ReliableTaildirEventReader.Builder() .filePaths(filePaths) + .filePathsIncludeChild(filePathsIncludeChild) .headerTable(headerTable) .positionFilePath(positionFilePath) .skipToEnd(skipToEnd) @@ -154,12 +156,27 @@ public String toString() { @Override public synchronized void configure(Context context) { String fileGroups = context.getString(FILE_GROUPS); - Preconditions.checkState(fileGroups != null, "Missing param: " + FILE_GROUPS); + String fileGroupsIncludeChild = context.getString(FILE_GROUPS_INCLUDE_CHILD); + Preconditions.checkState(fileGroups != null || + fileGroupsIncludeChild != null, "Missing param: " + FILE_GROUPS); + + Map filePathsMap = context.getSubProperties(FILE_GROUPS_PREFIX); + if (!filePathsMap.isEmpty()) { + filePaths = selectByKeys(filePathsMap, + fileGroups.split("\\s+")); + Preconditions.checkState(!filePaths.isEmpty(), + "Mapping for tailing files is empty or invalid: '" + FILE_GROUPS_PREFIX + "'"); + } - filePaths = selectByKeys(context.getSubProperties(FILE_GROUPS_PREFIX), - fileGroups.split("\\s+")); - Preconditions.checkState(!filePaths.isEmpty(), - "Mapping for tailing files is empty or invalid: '" + FILE_GROUPS_PREFIX + "'"); + Map filePathsIncludeChildMap = + context.getSubProperties(FILE_GROUPS_INCLUDE_CHILD_PREFIX); + if (!filePathsIncludeChildMap.isEmpty()) { + filePathsIncludeChild = selectByKeys(filePathsIncludeChildMap, + fileGroupsIncludeChild.split("\\s+")); + Preconditions.checkState(!filePathsIncludeChild.isEmpty(), + "Mapping for tailing files is empty or invalid: '" + + FILE_GROUPS_INCLUDE_CHILD_PREFIX + "'"); + } String homePath = System.getProperty("user.home").replace('\\', '/'); positionFilePath = context.getString(POSITION_FILE, homePath + DEFAULT_POSITION_FILE); diff --git a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSourceConfigurationConstants.java b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSourceConfigurationConstants.java index c614e26a5d..4294089cbe 100644 --- a/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSourceConfigurationConstants.java +++ b/flume-ng-sources/flume-taildir-source/src/main/java/org/apache/flume/source/taildir/TaildirSourceConfigurationConstants.java @@ -22,6 +22,10 @@ public class TaildirSourceConfigurationConstants { public static final String FILE_GROUPS = "filegroups"; public static final String FILE_GROUPS_PREFIX = FILE_GROUPS + "."; + /** Mapping for tailing file groups.(subdirectories are also monitored) */ + public static final String FILE_GROUPS_INCLUDE_CHILD = "filegroupsIncludeChild"; + public static final String FILE_GROUPS_INCLUDE_CHILD_PREFIX = FILE_GROUPS_INCLUDE_CHILD + "."; + /** Mapping for putting headers to events grouped by file groups. */ public static final String HEADERS_PREFIX = "headers.";