{"record":{"id":"752f0ee211f1bf92","repo":"apache/druid","slug":"tasklogdir-s-must-be-a-directory-752f0e","errorCode":null,"errorMessage":"taskLogDir [%s] must be a directory.","messagePattern":"taskLogDir \\[(.+?)\\] must be a directory\\.","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"indexing-service/src/main/java/org/apache/druid/indexing/common/tasklogs/FileTaskLogs.java","lineNumber":129,"sourceCode":"  {\n    return new File(config.getDirectory(), StringUtils.format(\"%s.%s\", taskid, filename));\n  }\n\n  @Override\n  public void killAll() throws IOException\n  {\n    log.info(\"Deleting all task logs from local dir [%s].\", config.getDirectory().getAbsolutePath());\n    FileUtils.deleteDirectory(config.getDirectory());\n  }\n\n  @Override\n  public void killOlderThan(final long timestamp) throws IOException\n  {\n    File taskLogDir = config.getDirectory();\n    if (taskLogDir.exists()) {\n\n      if (!taskLogDir.isDirectory()) {\n        throw new IOE(\"taskLogDir [%s] must be a directory.\", taskLogDir);\n      }\n\n      File[] files = taskLogDir.listFiles(f -> f.lastModified() < timestamp);\n\n      for (File file : files) {\n        log.info(\"Deleting local task log [%s].\", file.getAbsolutePath());\n        org.apache.commons.io.FileUtils.forceDelete(file);\n\n        if (Thread.currentThread().isInterrupted()) {\n          throw new IOException(\n              new InterruptedException(\"Thread interrupted. Couldn't delete all tasklogs.\")\n          );\n        }\n      }\n    }\n  }\n}\n","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/indexing-service/src/main/java/org/apache/druid/indexing/common/tasklogs/FileTaskLogs.java#L111-L147","documentation":"FileTaskLogs.killOlderThan deletes local task log files older than the given timestamp; before listing, it validates that the configured taskLogDir exists and is a directory. This IOE is thrown when config.getDirectory() points to an existing path that is a regular file (or other non-directory), so log cleanup cannot proceed. It indicates a bad druid.indexer.logs.directory configuration rather than a transient failure.","triggerScenarios":"druid.indexer.logs.directory (or equivalent FileTaskLogsConfig directory) resolves to an existing file, symlink to a file, or mount point that is not a directory when the middle manager/overlord runs killOlderThan for log retention.","commonSituations":"Log directory accidentally replaced by a file (e.g., a stray mount, a file created at the same path); misconfigured or typo'd path colliding with an existing file; container volume mounted incorrectly; permissions changes that left a file where the directory used to be.","solutions":["Inspect config.getDirectory() path on the affected node and remove or rename the non-directory entry, then recreate it as a directory: mkdir -p <path>","Correct druid.indexer.logs.directory in the node's runtime.properties to point to a proper directory and restart the service","Check volume/mount configuration if running in containers or on NFS","Optionally pre-validate with a startup check (Files.isDirectory) to fail fast at boot"],"exampleFix":"// before: only checked at killOlderThan time\nFile taskLogDir = config.getDirectory();\nif (!taskLogDir.isDirectory()) { throw new IOE(...); }\n// after: fail fast at startup\nPreConditions.checkArgument(Files.isDirectory(config.getDirectory().toPath()), \"taskLogDir must be a directory\");\n// or on the host: rm badpath && mkdir -p badpath","handlingStrategy":"validation","validationCode":"// pre-validate the configured log directory on service startup\nFile dir = config.getDirectory();\nif (dir.exists() && !dir.isDirectory()) {\n  throw new IllegalStateException(\"druid.indexer.logs.directory must be a directory: \" + dir);\n}","typeGuard":"static boolean isUsableLogDir(File dir) {\n  return dir != null && (!dir.exists() || dir.isDirectory());\n}","tryCatchPattern":"catch (IOException e) {\n  if (e.getMessage() != null && e.getMessage().startsWith(\"taskLogDir\")) {\n    log.error(\"fix druid.indexer.logs.directory (not a directory); skipping log cleanup this cycle\");\n    return; // don't crash retention loop; alert instead\n  } throw e;\n}","preventionTips":["Validate druid.indexer.logs.directory in startup health checks","Ensure container/volume mounts map to real directories, not files","Don't create files at the log directory path (watch for stray logs/redirects)","Verify the path on every node individually in multi-node clusters"],"tags":["task-logs","configuration","filesystem"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}