Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
820840d
first query for flow sheets
skeating Feb 2, 2026
fb3d6da
building up scripts
skeating Feb 4, 2026
8476351
adding more files
skeating Feb 19, 2026
47240ff
Merge branch 'dev' into sk/queries
skeating Aug 5, 2026
3961143
Add and refine SQL scripts for EHR data extraction
skeating Aug 9, 2026
cdec5ca
Started on functions to wrap snakemake around sql calls
thompson318 Aug 11, 2026
9897259
Started to implement caboodle look up code
thompson318 Aug 12, 2026
1f2cfbe
Moved airway sql to sql directory
thompson318 Aug 12, 2026
bd72361
Setting up config and docker bits
thompson318 Aug 12, 2026
5e303f0
Add a ehr location to snakefile
thompson318 Aug 17, 2026
89e9526
Tidied up implementation and rewrote Snakefile logic, with help from …
thompson318 Aug 17, 2026
e0498a2
We don't need a separate container for ehr lookup
thompson318 Aug 17, 2026
f7ac2db
Added ehr variables to settings and added a testing flag.
thompson318 Aug 17, 2026
291be43
added some caboodle faking logic to enable local running
thompson318 Aug 17, 2026
33e2c08
Caboodle setup goes into exporter.env
thompson318 Aug 18, 2026
9b291cb
Format fix
thompson318 Aug 18, 2026
d4fde59
Beginning of test for ehr integration
thompson318 Aug 18, 2026
d910831
SQL formatting
thompson318 Aug 18, 2026
654db91
Merge branch 'dev' into st/queries-to-snakemake
thompson318 Aug 18, 2026
e8f29a7
Test with ehr lookup
thompson318 Aug 18, 2026
86d5dea
use caboodle testing
thompson318 Aug 18, 2026
7fbcfc0
Trying to test ehr and hash lookup with ftps
thompson318 Aug 18, 2026
af487e6
Moved sql
thompson318 Aug 19, 2026
b535191
refactored db to make adding ehr lookup implementation clearer
thompson318 Aug 19, 2026
4f0ba6f
Started on logic to get flowsheets, first need hospital visit number
thompson318 Aug 19, 2026
88ba017
The exporter needs to access uds, do duplicated some of the config fr…
thompson318 Aug 19, 2026
72f4935
Use enviroment to manage sql locations as it varies between containers
thompson318 Aug 20, 2026
29865e0
path into test
thompson318 Aug 20, 2026
de94422
Use format rather than parameters for schema name
thompson318 Aug 20, 2026
518060b
Default for sql path to simplify testing
thompson318 Aug 20, 2026
03dbb60
Updated mocked function name in test controller
thompson318 Aug 20, 2026
c08785c
Enable a fake star db shortcut so that snakemake integration test can…
thompson318 Aug 24, 2026
cccf078
Don't connect for fake star
thompson318 Aug 24, 2026
1dfe77b
Merge branch 'dev' into st/queries-to-snakemake
thompson318 Aug 24, 2026
c84ca84
Refactor lab results query for wide format output
skeating Aug 24, 2026
9ae3715
Tidied up getting for hospital visit number
thompson318 Aug 26, 2026
920ae5c
Merge branch 'dev' into st/queries-to-snakemake
thompson318 Aug 26, 2026
bf90f90
return integer hospital visit id
thompson318 Aug 26, 2026
6fc3a70
Merge branch 'sk/queries' into st/queries-to-snakemake
thompson318 Aug 26, 2026
aa8f86a
Added flowsheet query to ehr
thompson318 Aug 26, 2026
5d06ecc
moved sql and implemented lab results
thompson318 Aug 27, 2026
c61164c
Fixed column names in pseudon
thompson318 Aug 27, 2026
a450faf
Flowsheets and labresults are star, not caboodle
thompson318 Aug 27, 2026
7b60b7d
removed queries from proprietary database
skeating Aug 28, 2026
83908c7
removed queries from proprietary database
skeating Aug 28, 2026
72f05a8
Merge branch 'st/queries-to-snakemake' into sk/queries
skeating Aug 31, 2026
613fa33
Auto stash before merge of "sk/queries" and "st/queries-to-snakemake"
skeating Aug 31, 2026
0763609
First go at janitor script. Only doing waveform processing files for
jeremyestein Sep 4, 2026
2b4a0d9
Merge remote-tracking branch 'origin/dev' into sk/queries
skeating Sep 7, 2026
6057e70
star flow sheet query
skeating Sep 7, 2026
fbfc355
finished star lab queries
skeating Sep 7, 2026
93fe4e7
Add test for janitor, and add ability to clear up save HL7
jeremyestein Sep 7, 2026
0560106
Docs tweak
jeremyestein Sep 7, 2026
392d579
Linting fix
jeremyestein Sep 7, 2026
a0624cf
Merge branch 'dev' into jeremy/janitoring
jeremyestein Sep 8, 2026
b1394c7
Rename namedtuple so pytest doesn't think it's a test class
jeremyestein Sep 8, 2026
7376d32
Copy the janitoring config file
jeremyestein Sep 8, 2026
b919c14
Was overriding coverage directories specified in config file on the
jeremyestein Sep 8, 2026
5eaa746
Merge remote-tracking branch 'origin/dev' into sk/queries
skeating Sep 8, 2026
87d9ae9
removed un new scripts and moved read me to current script location
skeating Sep 8, 2026
dfbb052
added note about getting scripts from the waveform-private-queries re…
skeating Sep 8, 2026
061dca7
Merge remote-tracking branch 'origin/jeremy/janitoring' into sk/queries
skeating Sep 8, 2026
2be6191
Files that it looks like didn't get merged properly
skeating Sep 8, 2026
89825d3
I missed one
skeating Sep 8, 2026
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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**/.venv
**/*.venv
.idea
.git
**/.mypy_cache
**/.pytest_tmp
4 changes: 3 additions & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ jobs:
cp config.EXAMPLE/hasher.env.EXAMPLE ../config/hasher.env
cp config.EXAMPLE/controller.env.EXAMPLE ../config/controller.env
cp config.EXAMPLE/monitoring.env.EXAMPLE ../config/monitoring.env
cp config.EXAMPLE/janitoring.env.EXAMPLE ../config/janitoring.env
# (lgtm is not involved in this test so its config file is not copied)
{
echo ""
echo "AZURE_CLIENT_ID=${AZURE_CLIENT_ID}"
Expand All @@ -67,7 +69,7 @@ jobs:

- name: Run the tests
working-directory: waveform-controller
run: uv run pytest tests --cov=src --cov-report=term-missing --cov-report=xml
run: uv run pytest --cov --cov-report=term-missing --cov-report=xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ wheels/

# IDEs
.idea/
.vscode/

# settings files (should not be in the source tree anyway, but just in case)
*.env
Expand Down
1 change: 1 addition & 0 deletions config.EXAMPLE/controller.env.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RABBITMQ_PASSWORD="my_pw"
RABBITMQ_HOST="localhost"
RABBITMQ_PORT=5672
RABBITMQ_QUEUE="waveform"
SQL_PATH="./src/sql/"
# OpenTelemetry OTLP/HTTP endpoint of the LGTM collector.
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318"
OTEL_SERVICE_NAME=waveform-controller
27 changes: 27 additions & 0 deletions config.EXAMPLE/exporter.env.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ ONLY_USE_CSV_FROM_YESTERDAY=TRUE
# expression to match multiple date
PROCESS_CSV_FROM_DATE=

# We query Caboodle to get electronic healthcare record date per patient per day
CABOODLE_DBNAME="fakecab"
CABOODLE_USERNAME="inform_user"
CABOODLE_PASSWORD="inform"
CABOODLE_HOST="localhost"
CABOODLE_PORT="5433"
CABOODLE_CONNECT_TIMEOUT="10" # in seconds
CABOODLE_QUERY_TIMEOUT="3000" # in milliseconds

# To avoid having to deploy a fake caboodle for testing we have
# a testing flag for Caboodle. If set TRUE caboodle connection will
# fail silently and ehr file will be created with fake data
CABOODLE_TESTING="FALSE"

# The following is duplicated from controller.env
# the exporter needs access to uds
UDS_DBNAME="fakeuds"
UDS_USERNAME="inform_user"
UDS_PASSWORD="inform"
UDS_HOST="172.17.0.1"
UDS_PORT="5433"
UDS_CONNECT_TIMEOUT="10"
UDS_QUERY_TIMEOUT="3000"
SCHEMA_NAME="schemaname"

SQL_PATH="/app/src/sql/"

# OpenTelemetry OTLP/HTTP endpoint of the LGTM collector.
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318"
OTEL_SERVICE_NAME=waveform-exporter
13 changes: 13 additions & 0 deletions config.EXAMPLE/janitoring.env.EXAMPLE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This is an EXAMPLE file, do not put real secrets in here.
# Copy it to ../config/janitoring.env and then DELETE THIS COMMENT.
# When does the janitoring job run
JANITORING_CRON_SCHEDULE="*/15 * * * *"
# OpenTelemetry OTLP/HTTP endpoint of the LGTM collector.
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318"
OTEL_SERVICE_NAME=waveform-janitoring

# Blank means do not delete
HL7_BZ2_ARCHIVE_RETENTION_DAYS=
ORIGINAL_CSV_RETENTION_DAYS=30
ORIGINAL_PARQUET_RETENTION_DAYS=30
PSEUDONYMISED_RETENTION_DAYS=30
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ services:
build:
context: .
dockerfile: monitoring/Dockerfile
target: supercronic_monitoring
args:
<<: *proxy-common
env_file:
Expand All @@ -79,3 +80,17 @@ services:
- ../../waveform-saved-messages:/waveform-saved-messages:ro
- ../waveform-export:/waveform-export:ro
restart: unless-stopped
waveform-janitoring:
build:
context: .
dockerfile: monitoring/Dockerfile
target: supercronic_janitoring
args:
<<: *proxy-common
env_file:
- ../config/janitoring.env
volumes:
# This will need to be rw if we ever enable deletion for real
- ../../waveform-saved-messages:/waveform-saved-messages:ro
- ../waveform-export:/waveform-export:ro
restart: unless-stopped
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sledgehammer approach which is rather similar to
* Delete all Emap tables in `star_dev` as per Emap deployment instructions.
* Waveform: `docker compose down` to bring everything down
* git pull and rebuild containers for the two repos.
* **REMEMBER: that the files from waveform-private-queries repository need to be copied to the waveform-controller/src/sql directory.**
* Change config if necessary
* Bring it all up again

Expand Down
42 changes: 42 additions & 0 deletions docs/janitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# File tidy up (waveform-janitoring)

## Suggested retention times

| Data | Size | Replaceability | Comment | Suggested deletion policy |
|--------|--------|--------|--------|--------|
| compressed HL7 | Large | Cannot be replaced | | Never delete unless under duress. If disk gets low, try to find more disk, or delete old stuff first if we have confidence that our uploaded data will not need to be re-processed. Archives are indexed by time and bed, but ALL variables are mixed together, so targeted deletion is only possible by time and bed. |
| original CSV | Large but see #15 | Can be reprocessed from HL7 | can be useful for debugging | Delete after 30 days or if disk gets low; consider targeted deletion (see #79) |
| original parquet | Small | Can be reprocessed from CSV | parquets are generated in a single step from CSV | Delete after 30 days or if disk gets low; consider targeted deletion (see #79) |
| pseduonymised parquet | Small | Can be reprocessed from CSV | parquets are generated in a single step from CSV | Delete after 30 days or if disk gets low; consider targeted deletion (see #79) |
| pre-upload tar files | Small | Is an intermediate file for upload | Not needed except for (short term) debugging | Delete after 30 days or if disk gets low; consider deleting immediately after upload |

Retention times for file types that are only useful for debugging could be shortened when we have more confidence in the pipeline.

## Design decisions

In the first iteration of this feature, retention times are evaluated against
file modification times. Ie. processing time.

It might, in future, be reasonable to also take observation time into account
(ie. using the dirname "2024-10-01" to determine when the data relates to).

For live data, these time stamps will be very similar so it doesn't matter which we use.

But if we reprocess some data from stored HL7 archives,
the modified times could be quite new vs the observation times.
If we only use modification times, a large amount of data could be produced quite quickly
that won't get cleared up, possibly leading to a full disk.
If we only use observation times and the data is sufficiently old, we could see CSV files being
deleted before we have a chance to convert and upload them.
Therefore it seems likely that to solve those problems we'd have to somehow take both into account.

There shouldn't be a scenario where observation times are newer than modification times
(bar synthetic data).

Snakemake has the ability to mark files as temporary. They are immediately deleted after they are needed.
However, we want to keep files for a certain time after snakemake has finished, so we can't use it.


## Configuration of janitoring service

See [janitoring config example file](../config.EXAMPLE/janitoring.env.EXAMPLE) for config variables.
17 changes: 10 additions & 7 deletions monitoring/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13-slim-bookworm@sha256:8092ae2ef67061f9db412458dbdce44dbf16748fb3cae5cdbd020f467a9712d0
FROM python:3.13-slim-bookworm@sha256:8092ae2ef67061f9db412458dbdce44dbf16748fb3cae5cdbd020f467a9712d0 AS supercronic_base
LABEL authors="Stephen Thompson, Jeremy Stein"

RUN export DEBIAN_FRONTEND=noninteractive && \
Expand Down Expand Up @@ -29,12 +29,15 @@ COPY --from=ghcr.io/astral-sh/uv@sha256:e85be844203885286c60ffad8a858d48afb6c5a5
ARG UVCACHE=/root/.cache/uv
WORKDIR /app

COPY monitoring/monitor.py monitoring/monitor.py.lock /app/
RUN uv lock --check --script monitor.py
COPY monitoring/* /app/
RUN chmod +x /app/*.sh

FROM supercronic_base AS supercronic_monitoring
RUN uv lock --check --script monitor.py
RUN --mount=type=cache,target=${UVCACHE} uv export --script monitor.py --locked | uv pip install --system -r -
ENTRYPOINT ["/app/monitoring_entrypoint.sh"]

COPY monitoring/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]
FROM supercronic_base AS supercronic_janitoring
RUN uv lock --check --script janitor.py
RUN --mount=type=cache,target=${UVCACHE} uv export --script janitor.py --locked | uv pip install --system -r -
ENTRYPOINT ["/app/janitoring_entrypoint.sh"]
136 changes: 136 additions & 0 deletions monitoring/janitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Scan saved HL7 messages and emit OpenTelemetry metrics.

Run in this command in dev to update the lockfile: `uv lock --script monitoring/janitor.py`
"""

import logging
import sys
from datetime import timedelta, datetime, timezone
from pathlib import Path
from time import perf_counter
from typing import Optional

from opentelemetry import metrics

import utils

# /// script
# requires-python = ">=3.13"
# dependencies = [
# "opentelemetry-exporter-otlp-proto-http==1.42.0",
# ]
# ///

INSTRUMENTATION_SCOPE = "waveform-janitoring.meter"
SAVED_MESSAGES_DIR = Path("/waveform-saved-messages")
WAVEFORM_EXPORT_DIR = Path("/waveform-export")

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
stream=sys.stdout,
)
logger = logging.getLogger(__name__)


def scan_waveform_exporter_files(meter, dry_run):
scan_time_hist = meter.create_histogram(
"waveform.janitoring.exporter.disk_cleanup_time",
unit="s",
description="Duration of cleanup in exporter directory",
)
start_time = perf_counter()
# Dirs that contain large files where we need to clean up.
# Missing/blank env means do not clean up at all.
big_top_level_dirs: dict[Path, Optional[float]] = {
WAVEFORM_EXPORT_DIR / "original-csv": utils.get_env(
"ORIGINAL_CSV_RETENTION_DAYS", as_type=float
),
WAVEFORM_EXPORT_DIR / "original-parquet": utils.get_env(
"ORIGINAL_PARQUET_RETENTION_DAYS", as_type=float
),
WAVEFORM_EXPORT_DIR / "pseudonymised": utils.get_env(
"PSEUDONYMISED_RETENTION_DAYS", as_type=float
),
SAVED_MESSAGES_DIR: utils.get_env(
"HL7_BZ2_ARCHIVE_RETENTION_DAYS", as_type=float
),
}
bytes_deleted_histo = meter.create_histogram(
"waveform.janitoring.deleted_bytes",
unit="By",
description="Bytes deleted by the janitoring process",
)
for tld_name, retention_days in big_top_level_dirs.items():
logger.info(f"Scanning {tld_name} for items older than {retention_days} days")
if not retention_days:
logger.info("Skipping %s due to empty/missing retention value", tld_name)
continue
tld = WAVEFORM_EXPORT_DIR / tld_name
tld_meter_name = tld_name.name.replace("-", "_")
byte_count = _delete_old_files(tld, retention_days, dry_run)
bytes_deleted_histo.record(
byte_count,
attributes={
"directory": tld_meter_name,
"dry_run": bool(dry_run),
},
)
time_taken = perf_counter() - start_time
scan_time_hist.record(time_taken)
logger.info("Scanned %s in %ss", WAVEFORM_EXPORT_DIR, time_taken)


def _delete_old_files(tld: Path, retention_days: float, dry_run) -> int:
retention_threshold = datetime.now(timezone.utc) - timedelta(days=retention_days)
retention_threshold_timestamp = retention_threshold.timestamp()
deleted_byte_count = 0
for dn, _, files in tld.walk():
for f in files:
f_path = dn / f
if f_path.is_file():
stat = f_path.stat()
actual_mtime = stat.st_mtime
if actual_mtime < retention_threshold_timestamp:
if not dry_run:
f_path.unlink()
logger.info(
"%sDeleting file [%s bytes] %s",
"[DRY RUN] " if dry_run else "",
stat.st_size,
f_path,
)
deleted_byte_count += stat.st_size
return deleted_byte_count


def main(args) -> int:
service_name = utils.get_env("OTEL_SERVICE_NAME")
otlp_endpoint = utils.get_env("OTEL_EXPORTER_OTLP_ENDPOINT")

# setup
utils.setup_metrics(service_name, otlp_endpoint)
meter = metrics.get_meter(INSTRUMENTATION_SCOPE)

# things to clean up
scan_waveform_exporter_files(meter, args.dry_run)

# shutdown, flush data
provider = metrics.get_meter_provider()

# (blank endpoint can return a provider that doesn't contain force_flush)
if hasattr(provider, "force_flush"):
provider.force_flush(timeout_millis=15000)

return 0


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()

raise SystemExit(main(args))
Loading
Loading