Skip to content

Commit 37387b1

Browse files
committed
Add logging framework and Tracer
1 parent 58000ca commit 37387b1

File tree

17 files changed

+514
-25
lines changed

17 files changed

+514
-25
lines changed

.vscode/launch.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@
3939
"request": "launch",
4040
"justMyCode": false,
4141
"module": "fastcs.demo",
42-
"args": ["run", "${workspaceFolder:FastCS}/src/fastcs/demo/controller.yaml"],
42+
"args": [
43+
"run",
44+
"${workspaceFolder:FastCS}/src/fastcs/demo/controller.yaml",
45+
"--log-level",
46+
"TRACE",
47+
// "--graylog-endpoint",
48+
// "graylog-log-target.diamond.ac.uk:12201",
49+
],
4350
"console": "integratedTerminal",
4451
}
4552
]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ dependencies = [
1919
"pydantic",
2020
"ruamel.yaml",
2121
"IPython",
22+
"loguru~=0.7",
23+
"pygelf",
2224
]
2325
dynamic = ["version"]
2426
license.file = "LICENSE"

src/fastcs/attributes.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Generic
77

88
import fastcs
9+
from fastcs.tracer import Tracer
910

1011
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
1112

@@ -21,7 +22,7 @@ class AttrMode(Enum):
2122
READ_WRITE = 3
2223

2324

24-
class _BaseAttrHandler:
25+
class _BaseAttrHandler(Tracer):
2526
async def initialise(self, controller: fastcs.controller.BaseController) -> None:
2627
pass
2728

@@ -62,7 +63,7 @@ async def update(self, attr: AttrR) -> None:
6263
raise RuntimeError("SimpleHandler cannot update")
6364

6465

65-
class Attribute(Generic[T]):
66+
class Attribute(Generic[T], Tracer):
6667
"""Base FastCS attribute.
6768
6869
Instances of this class added to a ``Controller`` will be used by the backend.
@@ -76,6 +77,8 @@ def __init__(
7677
handler: Any = None,
7778
description: str | None = None,
7879
) -> None:
80+
super().__init__()
81+
7982
assert issubclass(datatype.dtype, ATTRIBUTE_TYPES), (
8083
f"Attr type must be one of {ATTRIBUTE_TYPES}, "
8184
"received type {datatype.dtype}"
@@ -155,6 +158,8 @@ def get(self) -> T:
155158
return self._value
156159

157160
async def set(self, value: T) -> None:
161+
self.log_event("Attribute set", attribute=self, value=value)
162+
158163
self._value = self._datatype.validate(value)
159164

160165
if self._update_callbacks is not None:

src/fastcs/backend.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
from fastcs.cs_methods import Command, Put, Scan
66
from fastcs.datatypes import T
7+
from fastcs.logging import logger as _fastcs_logger
8+
from fastcs.tracer import Tracer
79

810
from .attributes import ONCE, AttrHandlerR, AttrHandlerW, AttrR, AttrW
911
from .controller import BaseController, Controller
1012
from .controller_api import ControllerAPI
1113
from .exceptions import FastCSError
1214
from .util import validate_hinted_attributes
1315

16+
tracer = Tracer(name=__name__)
17+
logger = _fastcs_logger.bind(logger_name=__name__)
18+
1419

1520
class Backend:
1621
"""For keeping track of tasks during FastCS serving."""
@@ -153,9 +158,14 @@ def _create_updater_callback(attribute: AttrR[T]):
153158

154159
async def callback():
155160
try:
161+
tracer.log_event("Call attribute updater", topic=attribute)
156162
await updater.update(attribute)
157-
except Exception as e:
158-
print(f"Update loop in {updater} stopped:\n{e.__class__.__name__}: {e}")
163+
except Exception:
164+
logger.opt(exception=True).error(
165+
"Update loop failed",
166+
updater=updater,
167+
attribute=attribute,
168+
)
159169
raise
160170

161171
return callback

src/fastcs/connections/ip_connection.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
from dataclasses import dataclass
33

4+
from fastcs.tracer import Tracer
5+
46

57
class DisconnectedError(Exception):
68
"""Raised if the ip connection is disconnected."""
@@ -44,10 +46,11 @@ async def close(self):
4446
await self.writer.wait_closed()
4547

4648

47-
class IPConnection:
49+
class IPConnection(Tracer):
4850
"""For connecting to an ip using a `StreamConnection`."""
4951

5052
def __init__(self):
53+
super().__init__()
5154
self.__connection = None
5255

5356
@property
@@ -61,14 +64,20 @@ async def connect(self, settings: IPConnectionSettings):
6164
reader, writer = await asyncio.open_connection(settings.ip, settings.port)
6265
self.__connection = StreamConnection(reader, writer)
6366

64-
async def send_command(self, message) -> None:
67+
async def send_command(self, message: str) -> None:
6568
async with self._connection as connection:
6669
await connection.send_message(message)
6770

68-
async def send_query(self, message) -> str:
71+
async def send_query(self, message: str) -> str:
6972
async with self._connection as connection:
7073
await connection.send_message(message)
71-
return await connection.receive_response()
74+
response = await connection.receive_response()
75+
self.log_event(
76+
"Received query response",
77+
query=message.strip(),
78+
response=response.strip(),
79+
)
80+
return response
7281

7382
async def close(self):
7483
async with self._connection as connection:

src/fastcs/controller.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from typing import get_type_hints
66

77
from fastcs.attributes import Attribute
8+
from fastcs.tracer import Tracer
89

910

10-
class BaseController:
11+
class BaseController(Tracer):
1112
"""Base class for controller."""
1213

1314
#: Attributes passed from the device at runtime.
@@ -18,6 +19,8 @@ class BaseController:
1819
def __init__(
1920
self, path: list[str] | None = None, description: str | None = None
2021
) -> None:
22+
super().__init__()
23+
2124
if (
2225
description is not None
2326
): # Use the argument over the one class defined description.

src/fastcs/cs_methods.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any, Generic, TypeVar
66

77
from fastcs.controller import BaseController
8+
from fastcs.tracer import Tracer
89

910
from .exceptions import FastCSError
1011

@@ -31,10 +32,12 @@
3132
)
3233

3334

34-
class Method(Generic[Controller_T]):
35+
class Method(Generic[Controller_T], Tracer):
3536
"""Generic base class for all FastCS Controller methods."""
3637

3738
def __init__(self, fn: MethodCallback, *, group: str | None = None) -> None:
39+
super().__init__()
40+
3841
self._docstring = getdoc(fn)
3942

4043
sig = signature(fn, eval_str=True)

src/fastcs/demo/controller.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ transport:
1010
port: 8083
1111
log_level: info
1212
- ca_ioc:
13-
pv_prefix: DEMO
13+
pv_prefix: GARYDEMO
1414
gui:
1515
title: Temperature Controller Demo
1616
output_path: ./demo.bob

src/fastcs/demo/controllers.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class TemperatureControllerHandler(AttrHandlerRW):
3030
update_period: float | None = 0.2
3131
_controller: TemperatureController | TemperatureRampController | None = None
3232

33+
def __post_init__(self):
34+
# Call __init__ of non-dataclass parent
35+
super().__init__()
36+
3337
async def initialise(self, controller: BaseController):
3438
assert isinstance(controller, TemperatureController | TemperatureRampController)
3539
self._controller = controller
@@ -42,15 +46,21 @@ def controller(self) -> TemperatureController | TemperatureRampController:
4246
return self._controller
4347

4448
async def put(self, attr: AttrW, value: Any) -> None:
45-
await self.controller.connection.send_command(
46-
f"{self.name}{self.controller.suffix}={attr.dtype(value)}\r\n"
47-
)
49+
command = f"{self.name}{self.controller.suffix}={attr.dtype(value)}"
50+
51+
await self.controller.connection.send_command(f"{command}\r\n")
52+
self.log_event("Put request for attribute", topic=attr, command=command)
4853

4954
async def update(self, attr: AttrR) -> None:
50-
response = await self.controller.connection.send_query(
51-
f"{self.name}{self.controller.suffix}?\r\n"
52-
)
55+
query = f"{self.name}{self.controller.suffix}?"
56+
response = await self.controller.connection.send_query(f"{query}\r\n")
5357
response = response.strip("\r\n")
58+
self.log_event(
59+
"Query for attribute",
60+
topic=attr,
61+
query=query,
62+
response=response,
63+
)
5464

5565
await attr.set(attr.dtype(response))
5666

@@ -87,10 +97,17 @@ async def close(self) -> None:
8797

8898
@scan(0.1)
8999
async def update_voltages(self):
100+
query = "V?"
90101
voltages = json.loads(
91-
(await self.connection.send_query("V?\r\n")).strip("\r\n")
102+
(await self.connection.send_query(f"{query}\r\n")).strip("\r\n")
92103
)
93104
for index, controller in enumerate(self._ramp_controllers):
105+
self.log_event(
106+
"Update voltages",
107+
topic=controller.voltage,
108+
query=query,
109+
response=voltages,
110+
)
94111
await controller.voltage.set(float(voltages[index]))
95112

96113

src/fastcs/launch.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
from ruamel.yaml import YAML
1414

1515
from fastcs import __version__
16+
from fastcs.logging import (
17+
GraylogEndpoint,
18+
GraylogEnvFields,
19+
GraylogStaticFields,
20+
LogLevel,
21+
configure_logging,
22+
logger,
23+
parse_graylog_env_fields,
24+
parse_graylog_static_fields,
25+
)
1626
from fastcs.transport.epics.ca.transport import EpicsCATransport
1727
from fastcs.transport.epics.pva.transport import EpicsPVATransport
1828
from fastcs.transport.graphql.transport import GraphQLTransport
@@ -86,6 +96,12 @@ async def serve(self) -> None:
8696

8797
coros.append(self._interactive_shell(context))
8898

99+
logger.info(
100+
"Starting FastCS",
101+
controller=self._controller,
102+
transports=f"[{', '.join(str(t) for t in self._transports)}]",
103+
)
104+
89105
try:
90106
await asyncio.gather(*coros)
91107
except asyncio.CancelledError:
@@ -199,10 +215,39 @@ def run(
199215
help=f"A yaml file matching the {controller_class.__name__} schema"
200216
),
201217
],
218+
log_level: Annotated[
219+
Optional[LogLevel], # noqa: UP045
220+
typer.Option(),
221+
] = None,
222+
graylog_endpoint: Annotated[
223+
Optional[GraylogEndpoint], # noqa: UP045
224+
typer.Option(
225+
help="Endpoint for graylog logging - '<host>:<port>'",
226+
parser=GraylogEndpoint.parse_graylog_endpoint,
227+
),
228+
] = None,
229+
graylog_static_fields: Annotated[
230+
Optional[GraylogStaticFields], # noqa: UP045
231+
typer.Option(
232+
help="Fields to add to graylog messages with static values",
233+
parser=parse_graylog_static_fields,
234+
),
235+
] = None,
236+
graylog_env_fields: Annotated[
237+
Optional[GraylogEnvFields], # noqa: UP045
238+
typer.Option(
239+
help="Fields to add to graylog messages from environment variables",
240+
parser=parse_graylog_env_fields,
241+
),
242+
] = None,
202243
):
203244
"""
204245
Start the controller
205246
"""
247+
configure_logging(
248+
log_level, graylog_endpoint, graylog_static_fields, graylog_env_fields
249+
)
250+
206251
controller_class = ctx.obj.controller_class
207252
fastcs_options = ctx.obj.fastcs_options
208253

0 commit comments

Comments
 (0)