Skip to content

Commit ea13a3c

Browse files
authored
Merge pull request #51 from ISISComputingGroup/add_logging
Add logging
2 parents efdc84c + f37d317 commit ea13a3c

File tree

27 files changed

+318
-99
lines changed

27 files changed

+318
-99
lines changed

.github/workflows/Lint-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: install requirements
2020
run: pip install -e .[dev]
2121
- name: run pytest
22-
run: python -m pytest
22+
run: IBEX_BLUESKY_CORE_LOGS=/tmp/ibex_bluesky_core_logs/ python -m pytest
2323
results:
2424
if: ${{ always() }}
2525
runs-on: ubuntu-latest

doc/dev/logging.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
# Logging
2+
To invoke the `ibex_bluesky_core` logger, create and use a `logger` object in [the standard way](https://docs.python.org/3/library/logging.html):
23

3-
To invoke the bluesky logger, import and use it at the desired level:
44
```python
5-
from ibex_bluesky_core.logger import logger
6-
logger.blueskylogger.warning("Message to be logged")
5+
import logging
6+
logger = logging.getLogger(__name__)
7+
logger.warning("Message to be logged")
78
```
8-
The logger utilizes a `TimedRotatingFileHandler` defined in the `logging.conf` file that rolls over the log at midnight.
99

10-
The default logging level is defined at `INFO`. This means that events of lesser severity will not be logged. To change the default level, change level attribute of logger_blueskycore in the `logging.conf`
10+
The logger utilizes a `TimedRotatingFileHandler` defined in `src/ibex_bluesky_core/log.py` that rolls over the log at midnight.
11+
12+
By default, the log files will be created in `c:\instrument\var\logs\bluesky`. This can be configured by setting
13+
the `IBEX_BLUESKY_CORE_LOGS` environment variable.
14+
15+
There are 3 primary logger objects which are "interesting" in the context of `ibex_bluesky_core`:
16+
- `ibex_bluesky_core` itself
17+
- `bluesky`, for low-level diagnostic logging from the run engine & plans
18+
- `ophyd_async` for low-level diagnostic logging from ophyd-async devices
19+
20+
The default logging level for bluesky libraries is defined at `INFO`. This means that events of lesser severity will not be logged.
21+
To change the logging level for all bluesky libraries simultaneously, call:
22+
23+
```python
24+
from ibex_bluesky_core.log import set_bluesky_log_levels
25+
set_bluesky_log_levels("DEBUG")
26+
```
27+
28+
To change the logging level of just a single library (for example, just `opyhyd_async`), use the standard
29+
python `logging` mechanisms:
30+
31+
```python
32+
import logging
33+
logging.getLogger("ophyd_async").setLevel("DEBUG")
34+
```

src/ibex_bluesky_core/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
"""Common plans and devices for use across beamlines."""
1+
"""Top-level ibex_bluesky_core module."""
2+
3+
from ibex_bluesky_core.log import setup_logging
4+
5+
__all__ = []
6+
7+
setup_logging()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""For callbacks that relate to the BlueSky run engine."""
1+
"""Bluesky callbacks which may be attached to the RunEngine."""

src/ibex_bluesky_core/callbacks/file_logger.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Creates a readable .txt file of Bluesky runengine dataset."""
22

33
import csv
4+
import logging
45
from datetime import datetime
56
from pathlib import Path
67
from typing import Optional
@@ -12,6 +13,8 @@
1213
from event_model.documents.run_stop import RunStop
1314
from zoneinfo import ZoneInfo
1415

16+
logger = logging.getLogger(__name__)
17+
1518
TIME = "time"
1619
START_TIME = "start_time"
1720
NAME = "name"
@@ -49,6 +52,8 @@ def start(self, doc: RunStart) -> None:
4952
self.current_start_document = doc[UID]
5053
self.filename = self.output_dir / f"{self.current_start_document}.txt"
5154

55+
logger.info("starting new file %s", self.filename)
56+
5257
exclude_list = [
5358
TIME, # We format this later
5459
]
@@ -64,19 +69,26 @@ def start(self, doc: RunStart) -> None:
6469
for key, value in header_data.items():
6570
outfile.write(f"{key}: {value}\n")
6671

72+
logger.debug("successfully wrote header in %s", self.filename)
73+
6774
def descriptor(self, doc: EventDescriptor) -> None:
6875
"""Add the descriptor data to descriptors."""
69-
if NAME not in doc or not doc[NAME] or doc[NAME] != "primary":
76+
logger.debug("event descriptor with name=%s", doc.get(NAME))
77+
if doc.get(NAME) != "primary":
7078
return
7179

80+
logger.debug("saving event descriptor with name=%s, id=%s", doc.get(NAME), doc.get(UID))
7281
descriptor_id = doc[UID]
7382
self.descriptors[descriptor_id] = doc
7483

7584
def event(self, doc: Event) -> Event:
7685
"""Append an event's output to the file."""
7786
if not self.filename:
78-
print("File has not been started yet - doing nothing")
87+
logger.error("File has not been started yet - doing nothing")
7988
return doc
89+
90+
logger.debug("Appending event document %s", doc.get(UID))
91+
8092
formatted_event_data = {}
8193
descriptor_id = doc[DESCRIPTOR]
8294
event_data = doc[DATA]
@@ -110,5 +122,6 @@ def event(self, doc: Event) -> Event:
110122

111123
def stop(self, doc: RunStop) -> RunStop | None:
112124
"""Clear descriptors."""
125+
logger.info("Stopping run, clearing descriptors, filename=%s", self.filename)
113126
self.descriptors.clear()
114127
return super().stop(doc)

src/ibex_bluesky_core/callbacks/fitting/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from bluesky.callbacks import LiveFit as _DefaultLiveFit
1010
from bluesky.callbacks.core import make_class_safe
1111

12+
logger = logging.getLogger(__name__)
13+
1214

1315
class FitMethod:
1416
"""Tell LiveFit how to fit to a scan. Has a Model function and a Guess function.
@@ -42,9 +44,6 @@ def __init__(
4244
self.guess = guess
4345

4446

45-
logger = logging.getLogger(__name__)
46-
47-
4847
@make_class_safe(logger=logger) # pyright: ignore (pyright doesn't understand this decorator)
4948
class LiveFit(_DefaultLiveFit):
5049
"""Live fit, customized for IBEX."""
@@ -85,10 +84,12 @@ def update_fit(self) -> None:
8584
None
8685
8786
"""
87+
logger.debug("updating guess for %s ", self.method)
8888
self.init_guess = self.method.guess(
8989
np.array(next(iter(self.independent_vars_data.values()))),
9090
np.array(self.ydata),
9191
# Calls the guess function on the set of data already collected in the run
9292
)
93+
logger.info("new guess for %s: %s", self.method, self.init_guess)
9394

9495
super().update_fit()

src/ibex_bluesky_core/callbacks/plotting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def _show_plot(self) -> None:
1919
# Play nicely with the "normal" backends too - only force show if we're
2020
# actually using our custom backend.
2121
if "genie_python" in matplotlib.get_backend():
22+
logger.debug("Explicitly show()ing plot for IBEX")
2223
plt.show()
2324

2425
def start(self, doc: RunStart) -> None:

src/ibex_bluesky_core/devices/block.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""ophyd-async devices and utilities for communicating with IBEX blocks."""
22

33
import asyncio
4+
import logging
45
from dataclasses import dataclass
56
from typing import Callable, Generic, Type, TypeVar
67

@@ -18,6 +19,8 @@
1819

1920
from ibex_bluesky_core.devices import get_pv_prefix
2021

22+
logger = logging.getLogger(__name__)
23+
2124
"""Block data type"""
2225
T = TypeVar("T")
2326

@@ -140,6 +143,10 @@ async def trigger(self) -> None:
140143
They do not do anything when triggered.
141144
"""
142145

146+
def __repr__(self) -> str:
147+
"""Debug representation of this block."""
148+
return f"{self.__class__.__name__}(name={self.name})"
149+
143150

144151
class BlockRw(BlockR[T], Movable):
145152
"""Device representing an IBEX read/write block of arbitrary data type."""
@@ -184,14 +191,19 @@ async def set(self, value: T) -> None:
184191
"""Set the setpoint of this block."""
185192

186193
async def do_set(setpoint: T) -> None:
194+
logger.info("Setting Block %s to %s", self.name, setpoint)
187195
await self.setpoint.set(
188196
setpoint, wait=self._write_config.use_completion_callback, timeout=None
189197
)
198+
logger.info("Got completion callback from setting block %s to %s", self.name, setpoint)
190199

191200
# Wait for the _set_success_func to return true.
192201
# This uses an "async for" to loop over items from observe_value, which is an async
193202
# generator. See documentation on "observe_value" or python "async for" for more details
194203
if self._write_config.set_success_func is not None:
204+
logger.info(
205+
"Waiting for set_success_func on setting block %s to %s", self.name, setpoint
206+
)
195207
async for actual_value in observe_value(self.readback):
196208
if self._write_config.set_success_func(setpoint, actual_value):
197209
break
@@ -202,9 +214,15 @@ async def set_and_settle(setpoint: T) -> None:
202214
else:
203215
await do_set(setpoint)
204216

217+
logger.info(
218+
"Waiting for configured settle time (%f seconds) on block %s",
219+
self._write_config.settle_time_s,
220+
self.name,
221+
)
205222
await asyncio.sleep(self._write_config.settle_time_s)
206223

207224
await set_and_settle(value)
225+
logger.info("block set complete %s value=%s", self.name, value)
208226

209227

210228
class BlockRwRbv(BlockRw[T], Locatable):
@@ -241,6 +259,7 @@ def __init__(
241259

242260
async def locate(self) -> Location[T]:
243261
"""Get the current 'location' of this block."""
262+
logger.info("locating block %s", self.name)
244263
actual, sp_rbv = await asyncio.gather(
245264
self.readback.get_value(),
246265
self.setpoint_readback.get_value(),
@@ -301,6 +320,10 @@ def __init__(
301320
# CS:SB:blockname here, we avoid a write access exception when moving a motor block.
302321
super().__init__(f"{prefix}CS:SB:{block_name}:SP:RBV", name=block_name)
303322

323+
def __repr__(self) -> str:
324+
"""Debug representation of this block."""
325+
return f"{self.__class__.__name__}(name={self.name})"
326+
304327

305328
def block_r(datatype: Type[T], block_name: str) -> BlockR[T]:
306329
"""Get a local read-only block for the current instrument.

src/ibex_bluesky_core/devices/dae/dae.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Dae(StandardReadable):
4848
def __init__(self, prefix: str, name: str = "DAE") -> None:
4949
"""Create a new Dae ophyd-async device."""
5050
dae_prefix = f"{prefix}DAE:"
51+
self._prefix = prefix
5152
self.good_uah: SignalR[float] = epics_signal_r(float, f"{dae_prefix}GOODUAH")
5253
self.count_rate: SignalR[float] = epics_signal_r(float, f"{dae_prefix}COUNTRATE")
5354
self.m_events: SignalR[float] = epics_signal_r(float, f"{dae_prefix}MEVENTS")
@@ -109,3 +110,7 @@ def __init__(self, prefix: str, name: str = "DAE") -> None:
109110
self.controls: DaeControls = DaeControls(dae_prefix)
110111

111112
super().__init__(name=name)
113+
114+
def __repr__(self) -> str:
115+
"""Get string representation of this class for debugging."""
116+
return f"{self.__class__.__name__}(name={self.name}, prefix={self._prefix})"

src/ibex_bluesky_core/devices/dae/dae_controls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""ophyd-async devices and utilities for the DAE run controls."""
22

3+
import logging
34
from enum import IntFlag
45

56
from bluesky.protocols import Movable
67
from ophyd_async.core import AsyncStatus, SignalW, SignalX, StandardReadable
78
from ophyd_async.epics.signal import epics_signal_w, epics_signal_x
89

10+
logger = logging.getLogger(__name__)
11+
912

1013
class DaeControls(StandardReadable):
1114
"""Subdevice for the DAE run controls."""
@@ -43,4 +46,6 @@ def __init__(self, dae_prefix: str, name: str = "") -> None:
4346
@AsyncStatus.wrap
4447
async def set(self, value: BeginRunExBits) -> None:
4548
"""Start a run with the specified bits - See BeginRunExBits."""
49+
logger.info("starting run with options %s", value)
4650
await self._raw_begin_run_ex.set(value, wait=True, timeout=None)
51+
logger.info("start run complete")

0 commit comments

Comments
 (0)