Skip to content

Commit 792a28d

Browse files
committed
Add logging framework and Tracer
1 parent dba6908 commit 792a28d

File tree

17 files changed

+517
-27
lines changed

17 files changed

+517
-27
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/attribute_io.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from fastcs.attribute_io_ref import AttributeIORef, AttributeIORefT
44
from fastcs.attributes import AttrR, AttrRW
55
from fastcs.datatypes import T
6+
from fastcs.tracer import Tracer
67

78

8-
class AttributeIO(Generic[T, AttributeIORefT]):
9+
class AttributeIO(Generic[T, AttributeIORefT], Tracer):
910
ref_type = AttributeIORef
1011

1112
def __init_subclass__(cls) -> None:

src/fastcs/attributes.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
from collections.abc import Callable
55
from typing import Generic
66

7-
from .attribute_io_ref import AttributeIORefT
8-
from .datatypes import ATTRIBUTE_TYPES, AttrSetCallback, AttrUpdateCallback, DataType, T
7+
from fastcs.attribute_io_ref import AttributeIORefT
8+
from fastcs.datatypes import (
9+
ATTRIBUTE_TYPES,
10+
AttrSetCallback,
11+
AttrUpdateCallback,
12+
DataType,
13+
T,
14+
)
15+
from fastcs.tracer import Tracer
916

1017
ONCE = float("inf")
1118
"""Special value to indicate that an attribute should be updated once on start up."""
1219

1320

14-
class Attribute(Generic[T, AttributeIORefT]):
21+
class Attribute(Generic[T, AttributeIORefT], Tracer):
1522
"""Base FastCS attribute.
1623
1724
Instances of this class added to a ``Controller`` will be used by the FastCS class.
@@ -24,6 +31,8 @@ def __init__(
2431
group: str | None = None,
2532
description: str | None = None,
2633
) -> None:
34+
super().__init__()
35+
2736
assert issubclass(datatype.dtype, ATTRIBUTE_TYPES), (
2837
f"Attr type must be one of {ATTRIBUTE_TYPES}, "
2938
"received type {datatype.dtype}"
@@ -101,6 +110,8 @@ def get(self) -> T:
101110
return self._value
102111

103112
async def set(self, value: T) -> None:
113+
self.log_event("Attribute set", attribute=self, value=value)
114+
104115
self._value = self._datatype.validate(value)
105116

106117
if self._on_set_callbacks is not None:

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
@@ -9,9 +9,10 @@
99
from fastcs.attribute_io_ref import AttributeIORefT
1010
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
1111
from fastcs.datatypes import T
12+
from fastcs.tracer import Tracer
1213

1314

14-
class BaseController:
15+
class BaseController(Tracer):
1516
"""Base class for controller."""
1617

1718
#: Attributes passed from the device at runtime.
@@ -25,6 +26,8 @@ def __init__(
2526
description: str | None = None,
2627
ios: Sequence[AttributeIO[T, AttributeIORefT]] | None = None,
2728
) -> None:
29+
super().__init__()
30+
2831
if (
2932
description is not None
3033
): # 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: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ class TemperatureControllerAttributeIORef(AttributeIORef):
3333
name: str
3434
update_period: float | None = 0.2
3535

36+
def __post_init__(self):
37+
# Call __init__ of non-dataclass parent
38+
super().__init__()
39+
3640

3741
class TemperatureControllerAttributeIO(
3842
AttributeIO[NumberT, TemperatureControllerAttributeIORef]
@@ -44,17 +48,22 @@ def __init__(self, connection: IPConnection, suffix: str):
4448
async def send(
4549
self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT
4650
) -> None:
47-
await self._connection.send_command(
48-
f"{attr.io_ref.name}{self.suffix}={attr.dtype(value)}\r\n"
49-
)
51+
command = f"{attr.io_ref.name}{self.suffix}={attr.dtype(value)}"
52+
await self._connection.send_command(f"{command}\r\n")
53+
self.log_event("Send command for attribute", topic=attr, command=command)
5054

5155
async def update(
5256
self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef]
5357
) -> None:
54-
response = await self._connection.send_query(
55-
f"{attr.io_ref.name}{self.suffix}?\r\n"
56-
)
58+
query = f"{attr.io_ref.name}{self.suffix}?"
59+
response = await self._connection.send_query(f"{query}?\r\n")
5760
response = response.strip("\r\n")
61+
self.log_event(
62+
"Query for attribute",
63+
topic=attr,
64+
query=query,
65+
response=response,
66+
)
5867

5968
await attr.set(attr.dtype(response))
6069

@@ -93,10 +102,17 @@ async def close(self) -> None:
93102

94103
@scan(0.1)
95104
async def update_voltages(self):
105+
query = "V?"
96106
voltages = json.loads(
97-
(await self.connection.send_query("V?\r\n")).strip("\r\n")
107+
(await self.connection.send_query(f"{query}\r\n")).strip("\r\n")
98108
)
99109
for index, controller in enumerate(self._ramp_controllers):
110+
self.log_event(
111+
"Update voltages",
112+
topic=controller.voltage,
113+
query=query,
114+
response=voltages,
115+
)
100116
await controller.voltage.set(float(voltages[index]))
101117

102118

src/fastcs/launch.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@
1515

1616
from fastcs import __version__
1717
from fastcs.attribute_io_ref import AttributeIORef
18+
from fastcs.logging import (
19+
GraylogEndpoint,
20+
GraylogEnvFields,
21+
GraylogStaticFields,
22+
LogLevel,
23+
configure_logging,
24+
parse_graylog_env_fields,
25+
parse_graylog_static_fields,
26+
)
27+
from fastcs.logging import logger as _fastcs_logger
28+
from fastcs.tracer import Tracer
1829
from fastcs.transport.epics.ca.transport import EpicsCATransport
1930
from fastcs.transport.epics.pva.transport import EpicsPVATransport
2031
from fastcs.transport.graphql.transport import GraphQLTransport
@@ -39,6 +50,9 @@
3950
| GraphQLTransport
4051
]
4152

53+
tracer = Tracer(name=__name__)
54+
logger = _fastcs_logger.bind(logger_name=__name__)
55+
4256

4357
class FastCS:
4458
"""For launching a controller with given transport(s) and keeping
@@ -149,6 +163,12 @@ async def serve(self) -> None:
149163

150164
coros.append(self._interactive_shell(context))
151165

166+
logger.info(
167+
"Starting FastCS",
168+
controller=self._controller,
169+
transports=f"[{', '.join(str(t) for t in self._transports)}]",
170+
)
171+
152172
try:
153173
await asyncio.gather(*coros)
154174
except asyncio.CancelledError:
@@ -237,9 +257,10 @@ def _add_attribute_updater_tasks(
237257
def _create_updater_callback(attribute: AttrR[T]):
238258
async def callback():
239259
try:
260+
tracer.log_event("Call attribute updater", topic=attribute)
240261
await attribute.update()
241-
except Exception as e:
242-
print(f"Update loop in {attribute} stopped:\n{e.__class__.__name__}: {e}")
262+
except Exception:
263+
logger.opt(exception=True).error("Update loop failed", attribute=attribute)
243264
raise
244265

245266
return callback
@@ -381,10 +402,39 @@ def run(
381402
help=f"A yaml file matching the {controller_class.__name__} schema"
382403
),
383404
],
405+
log_level: Annotated[
406+
Optional[LogLevel], # noqa: UP045
407+
typer.Option(),
408+
] = None,
409+
graylog_endpoint: Annotated[
410+
Optional[GraylogEndpoint], # noqa: UP045
411+
typer.Option(
412+
help="Endpoint for graylog logging - '<host>:<port>'",
413+
parser=GraylogEndpoint.parse_graylog_endpoint,
414+
),
415+
] = None,
416+
graylog_static_fields: Annotated[
417+
Optional[GraylogStaticFields], # noqa: UP045
418+
typer.Option(
419+
help="Fields to add to graylog messages with static values",
420+
parser=parse_graylog_static_fields,
421+
),
422+
] = None,
423+
graylog_env_fields: Annotated[
424+
Optional[GraylogEnvFields], # noqa: UP045
425+
typer.Option(
426+
help="Fields to add to graylog messages from environment variables",
427+
parser=parse_graylog_env_fields,
428+
),
429+
] = None,
384430
):
385431
"""
386432
Start the controller
387433
"""
434+
configure_logging(
435+
log_level, graylog_endpoint, graylog_static_fields, graylog_env_fields
436+
)
437+
388438
controller_class = ctx.obj.controller_class
389439
fastcs_options = ctx.obj.fastcs_options
390440

0 commit comments

Comments
 (0)