Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions docs/guides/installing-a-watcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,57 @@ The watcher can't reach the Data Hub API. Check:
### Files are detected but not uploading

- In **manual mode**, files are not uploaded until approved via the upload queue. Check the web dashboard.
- Check `~/.data-hub/watcher.log` for error details.
- Check the log file at `C:\ProgramData\DataHubWatcher\watcher.log` (Windows) or `~/.data-hub/watcher.log` (macOS/Linux) for error details.
- Verify your API token hasn't expired.

### Logs

The watcher writes rotating logs to `~/.data-hub/watcher.log` (10 MB, 5 backups). Check this file for detailed error information. You can also run with `--verbose` for debug-level console output:
The watcher writes rotating logs to:

- **Windows**: `C:\ProgramData\DataHubWatcher\watcher.log`
- **macOS / Linux**: `~/.data-hub/watcher.log`

Files are 10 MB each with 5 backups kept (`watcher.log.1` through `watcher.log.5`). Both the CLI `watch` command and the Windows service write to the same path so a single `Get-Content -Wait` (or `tail -F`) shows everything regardless of which entrypoint is running. You can also pass `--verbose` for debug-level console output on the CLI:

```sh
data-hub-watcher --verbose watch
```

#### Don't run `watch` alongside the service

Running `data-hub-watcher watch` interactively while the Windows service is also running is not supported — both processes would race on the same rotating log file (and on the same watch directory). Stop the service first if you need to run the CLI for debugging:

```powershell
data-hub-watcher service stop
data-hub-watcher --verbose watch
data-hub-watcher service start # when you're done
```

#### Bootstrap log for pre-dispatcher crashes

The service writes a separate `service-bootstrap.log` next to `watcher.log` that captures crashes happening before the service control dispatcher takes over — for example, a missing `pywin32`, a moved virtualenv, or a corrupt install. If `watcher.log` is empty after a crash, check `C:\ProgramData\DataHubWatcher\service-bootstrap.log` for the traceback.

#### Turning on debug logging for the service

Add `DATA_HUB_WATCHER_LOG_LEVEL=DEBUG` to the env file the service is registered against (typically `~/.data-hub/.env.<environment>`) and restart the service. No redeploy or `service reinstall` is needed.

#### Triaging a service that crashes immediately

If the service exits before writing anything to `watcher.log`, two read-only commands will surface the failure:

1. Query the Windows Application event log for entries from the watcher or the underlying Python service host:

```powershell
Get-WinEvent -FilterHashtable @{LogName='Application'; StartTime=(Get-Date).AddHours(-1)} |
Where-Object { $_.ProviderName -match 'Python|DataHubWatcher' -or $_.Message -match 'DataHubWatcher' } |
Format-List TimeCreated, ProviderName, Id, LevelDisplayName, Message
```

2. Run the service in the foreground from the venv `pywin32` itself ships:

```powershell
& "C:\path\to\venv\Scripts\python.exe" -m win32serviceutil debug DataHubWatcher
```

This bypasses the SCM, runs the same startup path the service uses, and prints the full traceback to the console — the fastest way to see why a phase-A/B crash is happening.

4 changes: 2 additions & 2 deletions docs/guides/upgrading-the-watcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ There is no separate "yank" step — a rolled-back release is still on PyPI and

The upgrade subprocess started but didn't end up running the new version on the next process startup. The `details.reason` field on the `update_failed` event tells you which sub-case fired:

- `**subprocess raised: …`** — `subprocess.run` itself raised before it could exec the upgrade command. Typically `FileNotFoundError` because `uv` isn't on PATH for the service account, or a permission error. Check `~/.data-hub/watcher.log` for the full traceback.
- `**subprocess raised: …`** — `subprocess.run` itself raised before it could exec the upgrade command. Typically `FileNotFoundError` because `uv` isn't on PATH for the service account, or a permission error. Check `C:\ProgramData\DataHubWatcher\watcher.log` (Windows) or `~/.data-hub/watcher.log` (macOS/Linux) for the full traceback.
- `**subprocess exited <N>**` — the upgrade command itself failed. The event details include the last 1000 bytes of stdout/stderr; the most common cause is a transient PyPI / mirror failure or the version not yet existing on the index (see the "don't bump ahead of publish" note above).
- `**expected '<target>' after upgrade, running '<current>'**` — the subprocess succeeded, the service restarted, but the running interpreter still imports the old version. Almost always means a stale `__pycache__` or a separate copy of the package on `sys.path`. Run `uv tool uninstall data-hub-watcher && uv tool install data-hub-watcher==<target>` as the service account.

Expand Down Expand Up @@ -189,7 +189,7 @@ The marker says the upgrade was dispatched via the worker, but the worker never

Whenever an upgrade fires, the watcher mirrors `uv`'s full stdout/stderr to disk in two places so a misleading dashboard event doesn't leave you guessing:

- **In-process upgrades (POSIX, Windows pip)** — every line of subprocess output is logged at `INFO` to `~/.data-hub/watcher.log` (or the platform equivalent), prefixed with `Upgrade subprocess stdout/stderr tail:`. This happens regardless of the subprocess return code, so a partial install where uv exits 0 but printed errors is still recoverable.
- **In-process upgrades (POSIX, Windows pip)** — every line of subprocess output is logged at `INFO` to the watcher log (`C:\ProgramData\DataHubWatcher\watcher.log` on Windows, `~/.data-hub/watcher.log` on macOS/Linux), prefixed with `Upgrade subprocess stdout/stderr tail:`. This happens regardless of the subprocess return code, so a partial install where uv exits 0 but printed errors is still recoverable.
- **Windows uv-tool worker** — every line of uv's output is echoed line-by-line to `~/.data-hub/upgrade-worker.log` with a UTC timestamp (`uv stdout: …` / `uv stderr: …`). Tail this file to watch an upgrade as it happens.

Both files survive a service restart — the watcher only ever appends to them — so an after-the-fact post-mortem of "what did uv actually say five minutes ago?" is always answerable from the lab PC without re-running the upgrade.
Expand Down
2 changes: 1 addition & 1 deletion docs/watcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ The watcher maintains a SQLite database at `~/.data-hub/watcher.db` with three t
- `runs` — tracks which run IDs have been reported and (in auto mode) when their files finished uploading. The most recent `reported_at` timestamp is what the auto-updater consults to gate restarts on a quiet-instrument window.
- `detected_files` — the file manifest for every reported run, keyed on `(run_id, relative_path)`. Lets the initial scan skip files that are already part of a reported run even in manual mode (where `uploaded_files` stays empty), and lets the run detector hydrate its in-memory state on startup so a restart doesn't re-POST runs the API has already seen.

Logs are written to `~/.data-hub/watcher.log` (10 MB rotating, 5 backups).
Logs are written to `C:\ProgramData\DataHubWatcher\watcher.log` on Windows and `~/.data-hub/watcher.log` on macOS/Linux (10 MB rotating, 5 backups). Both the CLI `watch` command and the Windows service write to the same file so a single `Get-Content -Wait` (or `tail -F`) covers all entry points.

### Initial scan identity

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion watcher/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "data-hub-watcher"
version = "0.2.7"
version = "0.2.8"
description = "File-watcher agent for lab instrument PCs that ingests data into Data Hub."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
17 changes: 8 additions & 9 deletions watcher/src/data_hub_watcher/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,15 @@ def _load_and_client(ctx: click.Context) -> tuple[WatcherConfig, DataHubClient,


def _setup_file_logging() -> None:
"""Add a RotatingFileHandler to the root logger (`~/.data-hub/watcher.log`)."""
from logging.handlers import RotatingFileHandler
"""Add a RotatingFileHandler to the root logger (``~/.data-hub/watcher.log``).

log_path = DEFAULT_CONFIG_DIR / "watcher.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
handler = RotatingFileHandler(
str(log_path), maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
)
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
logging.getLogger().addHandler(handler)
Delegates to the shared helper in :mod:`data_hub_watcher.logging_setup`
so the CLI ``watch`` path and the Windows-service entry point can't
drift on log location, rotation policy, or format.
"""
from data_hub_watcher.logging_setup import setup_file_logging

setup_file_logging()


# ---------------------------------------------------------------------------
Expand Down
39 changes: 39 additions & 0 deletions watcher/src/data_hub_watcher/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
import re
import sys
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

Expand Down Expand Up @@ -29,6 +30,44 @@ def _read_watcher_version() -> str:
DEFAULT_CONFIG_DIR = Path("~/.data-hub").expanduser()
DEFAULT_CONFIG_FILENAME = "config.yaml"
ENV_FILENAME = ".env"


def _resolve_watcher_log_dir() -> Path:
"""Pick a log directory that both the operator user and LocalSystem can write to.

On Windows, ``~/.data-hub`` expands to different paths depending on
the account running the process — ``C:\\Users\\<op>\\.data-hub`` for
the operator running the CLI, ``C:\\Windows\\System32\\config\\
systemprofile\\.data-hub`` for the LocalSystem account the service
runs as. That split means a tail of "the" log file shows only half
the story during a triage session. We instead anchor logs at
``C:\\ProgramData\\DataHubWatcher`` (the conventional Windows
location for cross-user app data), where both accounts have write
access by default.

On non-Windows we keep ``DEFAULT_CONFIG_DIR`` so dev/test runs on
macOS or Linux still write under the working tree's ``~/.data-hub``
rather than trying to create a Windows-only path.

The exact filename (``watcher.log``) is shared by the CLI ``watch``
command and the Windows service. Running both simultaneously on
the same host is not supported — see the troubleshooting guide for
why — so the shared-file rotation race is not a real concern in
operator deployments.
"""
if sys.platform == "win32":
# ``os.environ.get(name, default)`` returns the empty string
# when ``name`` is set to ``""`` rather than falling back to
# the default. Guard against that by treating an empty value
# as missing — otherwise ``Path("") / "DataHubWatcher"`` would
# silently produce a relative path that lands in whatever
# cwd the service happens to start from.
program_data = os.environ.get("ProgramData") or r"C:\ProgramData"
return Path(program_data) / "DataHubWatcher"
return DEFAULT_CONFIG_DIR


WATCHER_LOG_DIR = _resolve_watcher_log_dir()
SUPPORTED_ENVIRONMENTS: tuple[str, ...] = ("staging", "production", "preview")

HEARTBEAT_INTERVAL_SECONDS = 60
Expand Down
Loading