create_logger adds a new handler every call, so repeat callers emit each line N times
Maintainers usually reply within 1 day
@swaragvs is already working on this.
Since Oct 11, 2026.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
Research direction
The bug is in create_logger in bioengine/utils, which calls addHandler on the process-wide logger from logging.getLogger(name) every time it runs. Start by running the reproduction in the issue (call create_logger five times with one name, then count the handlers) to confirm the count grows. Done means repeated calls leave the same handler set in place, with a second log_file destination still possible through an explicit path, and a test in the repo's existing utils tests should cover the repeated-call case.
Written by the indexing model from the issue text.
Description
What goes wrong
Any service that asks for its logger more than once ends up printing every log line many times over, and the number grows the longer the service runs. A line printed once ends up in the log hundreds of times, which does not just make the output noisy: it makes the log unusable as a record of what happened, because a message repeated 259 times looks exactly like work that was done 259 times.
We hit this on a Chiron data server. Its background scan runs every 30 seconds and asked for its logger at the top of each pass. Two hours in, a preprocessing request that had been handled exactly once appeared 259 times in docker logs, against roughly 264 scans elapsed at the moment it ran. We only established that the work had happened once by counting the status files it had written.
Cause
bioengine/utils create_logger takes a process-wide logger from logging.getLogger(name) and then calls addHandler unconditionally:
def create_logger(name, level=logging.INFO, log_file=None):
logger = logging.getLogger(name)
logger.setLevel(level)
logger.propagate = False
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(...))
logger.addHandler(console_handler) # <-- every call
if log_file:
file_handler = logging.FileHandler(log_file) # <-- and another
logger.addHandler(file_handler)
return logger
logging.getLogger(name) returns the same object for the same name, so the handler list grows by one (or two, with log_file) per call and nothing ever removes the previous one. A caller that creates its logger inside a loop body, or in a function called repeatedly, has no way to tell from the signature that this is what happens: the name says create, and it reads like an idempotent accessor.
With log_file set this also leaks a file descriptor per call.
Reproducing
Against bioengine 0.16.1, inside the worker image:
import logging
from bioengine.utils import create_logger
for _ in range(5):
create_logger("HandlerLeakProbe")
print(len(logging.getLogger("HandlerLeakProbe").handlers))
Prints 5. Expected 1.
Suggested fix
Make create_logger idempotent for a given name and destination. Either return early when the logger already has handlers, or clear the handler list before attaching, or tag the handlers this function attaches and replace only those. Returning early is the smallest change and matches what callers evidently assume:
logger = logging.getLogger(name)
if getattr(logger, "_bioengine_configured", False):
logger.setLevel(level)
return logger
...
logger._bioengine_configured = True
A caller that genuinely wants a second destination can then be given an explicit way to add one, rather than getting it as a side effect of asking for the logger again.
Workaround
We cache the logger on our side and call create_logger once per process. That keeps our data server correct on any bioengine version, but every other service calling it in a loop has the same problem, so it seems worth fixing upstream.
Environment
- bioengine 0.16.1, running as the BioEngine Datasets proxy server inside
ghcr.io/aicell-lab/chiron-tabula:0.7.8 - Python 3.11
- Dominant language
- Python
- Stars
- 9
- Forks
- 4
- Avg merge
- 2h 10m
- Merged PRs (30d)
- 40
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from aicell-lab/bioengine
-
A brief Hypha outage permanently kills a running app, and the worker keeps reporting itself healthyOpen
Difficulty 5/5 Over a week Newbie friendliness 42/100
aicell-lab/bioengine#234 ·
Maintainers usually reply within 1 day
All issues in aicell-lab/bioengine
Similar issues
-
link-rot
Difficulty 1/5 Under an hour Newbie friendliness 67/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
developmentseed/titiler-cmr#265 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Insecure Brotli version <1.2.1Possibly taken @jamescw19 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 85/100
stac-utils/stac-fastapi-pgstac#438 · 1 reaction ·
-
bug triage
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day