thanos-io/thanos · error

read dir

Error message

read dir: %s

What it means

Wraps an os.ReadDir failure while the reloader walks a config directory in apply(). The reloader periodically re-reads the watched directory to detect config changes; if the directory itself cannot be listed, apply aborts with this wrapped error including the directory path.

Solutions

  1. Verify the watched directory path exists and is a directory (ls -ld <dir>)
  2. Fix filesystem permissions so the process user can read the directory
  3. If running on a K8s ConfigMap mount, ensure the mount point is not temporarily unmounted/remounted
  4. Point the reloader at a stable parent directory that always exists

Example fix

// before
watchDir := "/etc/thanos/rules.d"
// after — ensure dir exists before starting Watch
if _, err := os.Stat(watchDir); os.IsNotExist(err) {
    if err := os.MkdirAll(watchDir, 0o755); err != nil { log.Fatal(err) }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if info, err := os.Stat(walkDir); err != nil || !info.IsDir() {
    return fmt.Errorf("watch dir %s missing or not a directory", walkDir)
}

Type guard

func isDir(p string) bool {
    info, err := os.Stat(p)
    return err == nil && info.IsDir()
}

Try / catch

if _, err := os.ReadDir(walkDir); err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) && os.IsNotExist(err) {
        // recreate dir or skip this poll cycle and retry
    }
}

Prevention

When it happens

Trigger: Watch's apply() calls os.ReadDir(walkDir) on a configured config directory and the OS returns an error: directory deleted mid-run, permission denied on the directory, or walkDir is not a directory (e.g. a file or dangling symlink).

Common situations: Config mount (Kubernetes ConfigMap volume) remounted/removed while Thanos is running; directory permissions changed; path typo so walkDir points at a file.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/6c086e6058fa9bb5. Report an issue: GitHub.

Appendix: source

Thrown at pkg/reloader/reloader.go:438

	cfgDirsHash := make([][]byte, len(r.cfgDirs))
	cfgDirsChanged := len(r.lastCfgDirsHash) == 0 && len(r.cfgDirs) > 0
	for i, cfgDir := range r.cfgDirs {
		h := sha256.New()

		walkDir, err := filepath.EvalSymlinks(cfgDir.Dir)
		if err != nil {
			return errors.Wrap(err, "dir symlink eval")
		}
		outDir, err := filepath.EvalSymlinks(cfgDir.OutputDir)
		if err != nil {
			return errors.Wrap(err, "dir symlink eval")
		}

		cfgDirFiles := map[string]struct{}{}
		entries, err := os.ReadDir(walkDir)
		if err != nil {
			return errors.Wrapf(err, "read dir: %s", walkDir)
		}
		for _, entry := range entries {
			path := filepath.Join(walkDir, entry.Name())

			// Make sure to follow a symlink before checking if it is a directory.
			targetFile, err := os.Stat(path)
			if err != nil {
				return errors.Wrapf(err, "stat file: %s", path)
			}

			if targetFile.IsDir() {
				continue
			}

			if err := hashFile(h, path); err != nil {
				return errors.Wrapf(err, "build hash for file: %s", path)
			}

View on GitHub (pinned to 35b8b99117)