nost-tools 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nost-tools might be problematic. Click here for more details.
- nost_tools/__init__.py +29 -29
- nost_tools/application.py +800 -793
- nost_tools/application_utils.py +262 -262
- nost_tools/configuration.py +304 -304
- nost_tools/entity.py +73 -73
- nost_tools/errors.py +14 -14
- nost_tools/logger_application.py +192 -192
- nost_tools/managed_application.py +261 -261
- nost_tools/manager.py +472 -472
- nost_tools/observer.py +181 -181
- nost_tools/publisher.py +141 -141
- nost_tools/schemas.py +432 -426
- nost_tools/simulator.py +531 -531
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/METADATA +118 -119
- nost_tools-2.0.1.dist-info/RECORD +18 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/licenses/LICENSE +29 -29
- nost_tools-2.0.0.dist-info/RECORD +0 -18
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/WHEEL +0 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/top_level.txt +0 -0
nost_tools/entity.py
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides a base class to maintain state variables during scenario execution.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from datetime import datetime, timedelta
|
|
7
|
-
|
|
8
|
-
from .observer import Observable
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Entity(Observable):
|
|
14
|
-
"""
|
|
15
|
-
A base entity that maintains its own clock (time) during scenario execution.
|
|
16
|
-
|
|
17
|
-
Notifies observers of changes to one observable property
|
|
18
|
-
* `time`: current scenario time
|
|
19
|
-
|
|
20
|
-
Attributes:
|
|
21
|
-
name (str): The entity name (optional)
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
PROPERTY_TIME = "time"
|
|
25
|
-
|
|
26
|
-
def __init__(self, name: str = None):
|
|
27
|
-
"""
|
|
28
|
-
Initializes a new entity.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
name (str): name of the entity (default: None)
|
|
32
|
-
"""
|
|
33
|
-
super().__init__()
|
|
34
|
-
self.name = name
|
|
35
|
-
self._init_time = self._time = self._next_time = None
|
|
36
|
-
|
|
37
|
-
def initialize(self, init_time: datetime) -> None:
|
|
38
|
-
"""
|
|
39
|
-
Initializes the entity at a designated initial scenario time.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
init_time (:obj:`datetime`): initial scenario time
|
|
43
|
-
"""
|
|
44
|
-
self._init_time = self._time = self._next_time = init_time
|
|
45
|
-
|
|
46
|
-
def tick(self, time_step: timedelta) -> None:
|
|
47
|
-
"""
|
|
48
|
-
Computes the next state transition following an elapsed scenario duration (time step).
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
time_step (:obj:`timedelta`): elapsed scenario duration
|
|
52
|
-
"""
|
|
53
|
-
self._next_time = self._time + time_step
|
|
54
|
-
|
|
55
|
-
def tock(self) -> None:
|
|
56
|
-
"""
|
|
57
|
-
Commits the state transition pre-computed in `tick` and notifies observers of changes.
|
|
58
|
-
"""
|
|
59
|
-
# update the time
|
|
60
|
-
if self._time != self._next_time:
|
|
61
|
-
prev_time = self._time
|
|
62
|
-
self._time = self._next_time
|
|
63
|
-
logger.debug(f"Entity {self.name} updated time to {self._time}.")
|
|
64
|
-
self.notify_observers(self.PROPERTY_TIME, prev_time, self._time)
|
|
65
|
-
|
|
66
|
-
def get_time(self) -> datetime:
|
|
67
|
-
"""
|
|
68
|
-
Retrieves the current scenario time.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
:obj:`datetime`: current scenario time
|
|
72
|
-
"""
|
|
73
|
-
return self._time
|
|
1
|
+
"""
|
|
2
|
+
Provides a base class to maintain state variables during scenario execution.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
from .observer import Observable
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Entity(Observable):
|
|
14
|
+
"""
|
|
15
|
+
A base entity that maintains its own clock (time) during scenario execution.
|
|
16
|
+
|
|
17
|
+
Notifies observers of changes to one observable property
|
|
18
|
+
* `time`: current scenario time
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
name (str): The entity name (optional)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
PROPERTY_TIME = "time"
|
|
25
|
+
|
|
26
|
+
def __init__(self, name: str = None):
|
|
27
|
+
"""
|
|
28
|
+
Initializes a new entity.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name (str): name of the entity (default: None)
|
|
32
|
+
"""
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.name = name
|
|
35
|
+
self._init_time = self._time = self._next_time = None
|
|
36
|
+
|
|
37
|
+
def initialize(self, init_time: datetime) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initializes the entity at a designated initial scenario time.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
init_time (:obj:`datetime`): initial scenario time
|
|
43
|
+
"""
|
|
44
|
+
self._init_time = self._time = self._next_time = init_time
|
|
45
|
+
|
|
46
|
+
def tick(self, time_step: timedelta) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Computes the next state transition following an elapsed scenario duration (time step).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
time_step (:obj:`timedelta`): elapsed scenario duration
|
|
52
|
+
"""
|
|
53
|
+
self._next_time = self._time + time_step
|
|
54
|
+
|
|
55
|
+
def tock(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Commits the state transition pre-computed in `tick` and notifies observers of changes.
|
|
58
|
+
"""
|
|
59
|
+
# update the time
|
|
60
|
+
if self._time != self._next_time:
|
|
61
|
+
prev_time = self._time
|
|
62
|
+
self._time = self._next_time
|
|
63
|
+
logger.debug(f"Entity {self.name} updated time to {self._time}.")
|
|
64
|
+
self.notify_observers(self.PROPERTY_TIME, prev_time, self._time)
|
|
65
|
+
|
|
66
|
+
def get_time(self) -> datetime:
|
|
67
|
+
"""
|
|
68
|
+
Retrieves the current scenario time.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
:obj:`datetime`: current scenario time
|
|
72
|
+
"""
|
|
73
|
+
return self._time
|
nost_tools/errors.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides object models for common data structures.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
class ConfigurationError(Exception):
|
|
6
|
-
"""Configuration error"""
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class EnvironmentVariableError(Exception):
|
|
10
|
-
"""Environment variable error"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ConfigAssertionError(Exception): # Renamed to avoid shadowing built-in
|
|
14
|
-
"""Assertion error for configuration validation"""
|
|
1
|
+
"""
|
|
2
|
+
Provides object models for common data structures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class ConfigurationError(Exception):
|
|
6
|
+
"""Configuration error"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EnvironmentVariableError(Exception):
|
|
10
|
+
"""Environment variable error"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigAssertionError(Exception): # Renamed to avoid shadowing built-in
|
|
14
|
+
"""Assertion error for configuration validation"""
|
nost_tools/logger_application.py
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides a base logger application that subscribes and writes all messages to file.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
from datetime import datetime, timedelta
|
|
8
|
-
|
|
9
|
-
from .application import Application
|
|
10
|
-
from .configuration import ConnectionConfig
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class LoggerApplication(Application):
|
|
16
|
-
"""
|
|
17
|
-
Logger NOS-T Application.
|
|
18
|
-
|
|
19
|
-
This object class defines the basic functionality for a NOS-T application
|
|
20
|
-
that subscribes to a specified topic and logs all messages to file.
|
|
21
|
-
|
|
22
|
-
Attributes:
|
|
23
|
-
prefix (str): The test run namespace (prefix)
|
|
24
|
-
simulator (:obj:`Simulator`): Application simulator defined in the *Simulator* class
|
|
25
|
-
app_name (str): Logger application name (default: logger)
|
|
26
|
-
app_description (str): Logger application description (optional)
|
|
27
|
-
log_app (str): Application name to be logged (default: "+")
|
|
28
|
-
log_topic (str): Topic to be logged (default: "#")
|
|
29
|
-
log_dir (str): Directory to write log files (default: ".")
|
|
30
|
-
log_file (:obj:`File`): Current log file
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(self, app_name: str = "logger", app_description: str = None):
|
|
34
|
-
"""
|
|
35
|
-
Initializes a new logging application.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
app_name (str): application name (default: "logger")
|
|
39
|
-
app_description (str): application description (optional)
|
|
40
|
-
"""
|
|
41
|
-
super().__init__(app_name, app_description)
|
|
42
|
-
self.log_topic = None
|
|
43
|
-
self.log_app = None
|
|
44
|
-
self.log_dir = None
|
|
45
|
-
self.log_file = None
|
|
46
|
-
|
|
47
|
-
def start_up(
|
|
48
|
-
self,
|
|
49
|
-
prefix: str,
|
|
50
|
-
config: ConnectionConfig,
|
|
51
|
-
set_offset: bool = None,
|
|
52
|
-
time_status_step: timedelta = None,
|
|
53
|
-
time_status_init: datetime = None,
|
|
54
|
-
shut_down_when_terminated: bool = None,
|
|
55
|
-
time_step: timedelta = None,
|
|
56
|
-
manager_app_name: str = None,
|
|
57
|
-
log_app: str = "+",
|
|
58
|
-
log_topic: str = "#",
|
|
59
|
-
log_dir: str = ".",
|
|
60
|
-
) -> None:
|
|
61
|
-
"""
|
|
62
|
-
Starts up the logger application by connecting to message broker,
|
|
63
|
-
starting a background event loop, subscribing to manager events, and
|
|
64
|
-
registering callback functions.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
prefix (str): The test run namespace (prefix)
|
|
68
|
-
config (:obj:`ConnectionConfig`): The connection configuration
|
|
69
|
-
set_offset (bool): True, if the system clock offset shall be set
|
|
70
|
-
time_status_step (:obj:`timedelta`): Time interval for status messages
|
|
71
|
-
time_status_init (:obj:`datetime`): Initial time for status messages
|
|
72
|
-
shut_down_when_terminated (bool): True, if the application shall shut down when terminated
|
|
73
|
-
time_step (:obj:`timedelta`): Time step for the application
|
|
74
|
-
manager_app_name (str): Manager application name
|
|
75
|
-
log_app (str): Application name to be logged (default: "+")
|
|
76
|
-
log_topic (str): Topic to be logged (default: "#")
|
|
77
|
-
log_dir (str): Directory to write log files (default: ".")
|
|
78
|
-
"""
|
|
79
|
-
if (
|
|
80
|
-
set_offset is not None
|
|
81
|
-
and time_status_step is not None
|
|
82
|
-
and time_status_init is not None
|
|
83
|
-
and shut_down_when_terminated is not None
|
|
84
|
-
and time_step is not None
|
|
85
|
-
and manager_app_name is not None
|
|
86
|
-
):
|
|
87
|
-
self.set_offset = set_offset
|
|
88
|
-
self.time_status_step = time_status_step
|
|
89
|
-
self.time_status_init = time_status_init
|
|
90
|
-
self.shut_down_when_terminated = shut_down_when_terminated
|
|
91
|
-
self.time_step = time_step
|
|
92
|
-
self.manager_app_name = manager_app_name
|
|
93
|
-
else:
|
|
94
|
-
self.config = config
|
|
95
|
-
parameters = getattr(
|
|
96
|
-
self.config.rc.simulation_configuration.execution_parameters,
|
|
97
|
-
"logger_application",
|
|
98
|
-
None,
|
|
99
|
-
)
|
|
100
|
-
self.set_offset = parameters.set_offset
|
|
101
|
-
self.time_status_step = parameters.time_status_step
|
|
102
|
-
self.time_status_init = parameters.time_status_init
|
|
103
|
-
self.shut_down_when_terminated = parameters.shut_down_when_terminated
|
|
104
|
-
|
|
105
|
-
self.log_app = log_app
|
|
106
|
-
self.log_topic = log_topic
|
|
107
|
-
self.log_dir = log_dir
|
|
108
|
-
|
|
109
|
-
# Create log directory if it doesn't exist
|
|
110
|
-
os.makedirs(self.log_dir, exist_ok=True)
|
|
111
|
-
|
|
112
|
-
# Open log file now
|
|
113
|
-
self._open_log_file()
|
|
114
|
-
|
|
115
|
-
# Start up base application
|
|
116
|
-
super().start_up(
|
|
117
|
-
prefix,
|
|
118
|
-
config,
|
|
119
|
-
self.set_offset,
|
|
120
|
-
self.time_status_step,
|
|
121
|
-
self.time_status_init,
|
|
122
|
-
self.shut_down_when_terminated,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# Add callback for specified topic(s)
|
|
126
|
-
self.add_message_callback(self.log_app, self.log_topic, self.on_log_message)
|
|
127
|
-
|
|
128
|
-
logger.info(f"Logger {self.app_name} started up and listening for messages.")
|
|
129
|
-
|
|
130
|
-
def shut_down(self) -> None:
|
|
131
|
-
"""
|
|
132
|
-
Shuts down the application by stopping the background event loop
|
|
133
|
-
and disconnecting from the message broker.
|
|
134
|
-
"""
|
|
135
|
-
# Close the log file if it's open
|
|
136
|
-
self._close_log_file()
|
|
137
|
-
|
|
138
|
-
# Shut down base application
|
|
139
|
-
super().shut_down()
|
|
140
|
-
|
|
141
|
-
def _open_log_file(self) -> None:
|
|
142
|
-
"""
|
|
143
|
-
Opens a new log file for writing messages.
|
|
144
|
-
"""
|
|
145
|
-
if self.log_file is not None:
|
|
146
|
-
self._close_log_file()
|
|
147
|
-
|
|
148
|
-
ts = (
|
|
149
|
-
str(self.simulator.get_wallclock_time()).replace(" ", "T").replace(":", "-")
|
|
150
|
-
)
|
|
151
|
-
log_filename = os.path.join(self.log_dir, f"{ts}.log")
|
|
152
|
-
self.log_file = open(log_filename, "a")
|
|
153
|
-
self.log_file.write(f"Timestamp,Topic,Payload\n")
|
|
154
|
-
logger.info(f"Logger {self.app_name} opened file {self.log_file.name}.")
|
|
155
|
-
|
|
156
|
-
def _close_log_file(self) -> None:
|
|
157
|
-
"""
|
|
158
|
-
Closes the current log file if it's open.
|
|
159
|
-
"""
|
|
160
|
-
if self.log_file is not None:
|
|
161
|
-
self.log_file.close()
|
|
162
|
-
logger.info(f"Logger {self.app_name} closed file {self.log_file.name}.")
|
|
163
|
-
self.log_file = None
|
|
164
|
-
|
|
165
|
-
def on_log_message(self, ch, method, properties, body):
|
|
166
|
-
"""
|
|
167
|
-
Callback function to log a message received by the logger application.
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
ch: The channel object
|
|
171
|
-
method: The method frame
|
|
172
|
-
properties: The message properties
|
|
173
|
-
body: The message body
|
|
174
|
-
"""
|
|
175
|
-
if self.log_file is not None:
|
|
176
|
-
try:
|
|
177
|
-
routing_key = method.routing_key
|
|
178
|
-
payload = body.decode("utf-8") if isinstance(body, bytes) else str(body)
|
|
179
|
-
|
|
180
|
-
logger.debug(f"Logger {self.app_name} logging message: {payload}")
|
|
181
|
-
|
|
182
|
-
timestamp = self.simulator.get_wallclock_time()
|
|
183
|
-
self.log_file.write(f"{timestamp},{routing_key},{payload}\n")
|
|
184
|
-
self.log_file.flush() # Ensure data is written immediately
|
|
185
|
-
except Exception as e:
|
|
186
|
-
logger.error(f"Error logging message: {e}")
|
|
187
|
-
else:
|
|
188
|
-
# If log file isn't open, try to reopen it
|
|
189
|
-
self._open_log_file()
|
|
190
|
-
logger.error(f"Logger {self.app_name} had to reopen log file.")
|
|
191
|
-
# Try to log the message again
|
|
192
|
-
self.on_log_message(ch, method, properties, body)
|
|
1
|
+
"""
|
|
2
|
+
Provides a base logger application that subscribes and writes all messages to file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from .application import Application
|
|
10
|
+
from .configuration import ConnectionConfig
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LoggerApplication(Application):
|
|
16
|
+
"""
|
|
17
|
+
Logger NOS-T Application.
|
|
18
|
+
|
|
19
|
+
This object class defines the basic functionality for a NOS-T application
|
|
20
|
+
that subscribes to a specified topic and logs all messages to file.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
prefix (str): The test run namespace (prefix)
|
|
24
|
+
simulator (:obj:`Simulator`): Application simulator defined in the *Simulator* class
|
|
25
|
+
app_name (str): Logger application name (default: logger)
|
|
26
|
+
app_description (str): Logger application description (optional)
|
|
27
|
+
log_app (str): Application name to be logged (default: "+")
|
|
28
|
+
log_topic (str): Topic to be logged (default: "#")
|
|
29
|
+
log_dir (str): Directory to write log files (default: ".")
|
|
30
|
+
log_file (:obj:`File`): Current log file
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, app_name: str = "logger", app_description: str = None):
|
|
34
|
+
"""
|
|
35
|
+
Initializes a new logging application.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
app_name (str): application name (default: "logger")
|
|
39
|
+
app_description (str): application description (optional)
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(app_name, app_description)
|
|
42
|
+
self.log_topic = None
|
|
43
|
+
self.log_app = None
|
|
44
|
+
self.log_dir = None
|
|
45
|
+
self.log_file = None
|
|
46
|
+
|
|
47
|
+
def start_up(
|
|
48
|
+
self,
|
|
49
|
+
prefix: str,
|
|
50
|
+
config: ConnectionConfig,
|
|
51
|
+
set_offset: bool = None,
|
|
52
|
+
time_status_step: timedelta = None,
|
|
53
|
+
time_status_init: datetime = None,
|
|
54
|
+
shut_down_when_terminated: bool = None,
|
|
55
|
+
time_step: timedelta = None,
|
|
56
|
+
manager_app_name: str = None,
|
|
57
|
+
log_app: str = "+",
|
|
58
|
+
log_topic: str = "#",
|
|
59
|
+
log_dir: str = ".",
|
|
60
|
+
) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Starts up the logger application by connecting to message broker,
|
|
63
|
+
starting a background event loop, subscribing to manager events, and
|
|
64
|
+
registering callback functions.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
prefix (str): The test run namespace (prefix)
|
|
68
|
+
config (:obj:`ConnectionConfig`): The connection configuration
|
|
69
|
+
set_offset (bool): True, if the system clock offset shall be set
|
|
70
|
+
time_status_step (:obj:`timedelta`): Time interval for status messages
|
|
71
|
+
time_status_init (:obj:`datetime`): Initial time for status messages
|
|
72
|
+
shut_down_when_terminated (bool): True, if the application shall shut down when terminated
|
|
73
|
+
time_step (:obj:`timedelta`): Time step for the application
|
|
74
|
+
manager_app_name (str): Manager application name
|
|
75
|
+
log_app (str): Application name to be logged (default: "+")
|
|
76
|
+
log_topic (str): Topic to be logged (default: "#")
|
|
77
|
+
log_dir (str): Directory to write log files (default: ".")
|
|
78
|
+
"""
|
|
79
|
+
if (
|
|
80
|
+
set_offset is not None
|
|
81
|
+
and time_status_step is not None
|
|
82
|
+
and time_status_init is not None
|
|
83
|
+
and shut_down_when_terminated is not None
|
|
84
|
+
and time_step is not None
|
|
85
|
+
and manager_app_name is not None
|
|
86
|
+
):
|
|
87
|
+
self.set_offset = set_offset
|
|
88
|
+
self.time_status_step = time_status_step
|
|
89
|
+
self.time_status_init = time_status_init
|
|
90
|
+
self.shut_down_when_terminated = shut_down_when_terminated
|
|
91
|
+
self.time_step = time_step
|
|
92
|
+
self.manager_app_name = manager_app_name
|
|
93
|
+
else:
|
|
94
|
+
self.config = config
|
|
95
|
+
parameters = getattr(
|
|
96
|
+
self.config.rc.simulation_configuration.execution_parameters,
|
|
97
|
+
"logger_application",
|
|
98
|
+
None,
|
|
99
|
+
)
|
|
100
|
+
self.set_offset = parameters.set_offset
|
|
101
|
+
self.time_status_step = parameters.time_status_step
|
|
102
|
+
self.time_status_init = parameters.time_status_init
|
|
103
|
+
self.shut_down_when_terminated = parameters.shut_down_when_terminated
|
|
104
|
+
|
|
105
|
+
self.log_app = log_app
|
|
106
|
+
self.log_topic = log_topic
|
|
107
|
+
self.log_dir = log_dir
|
|
108
|
+
|
|
109
|
+
# Create log directory if it doesn't exist
|
|
110
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
# Open log file now
|
|
113
|
+
self._open_log_file()
|
|
114
|
+
|
|
115
|
+
# Start up base application
|
|
116
|
+
super().start_up(
|
|
117
|
+
prefix,
|
|
118
|
+
config,
|
|
119
|
+
self.set_offset,
|
|
120
|
+
self.time_status_step,
|
|
121
|
+
self.time_status_init,
|
|
122
|
+
self.shut_down_when_terminated,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Add callback for specified topic(s)
|
|
126
|
+
self.add_message_callback(self.log_app, self.log_topic, self.on_log_message)
|
|
127
|
+
|
|
128
|
+
logger.info(f"Logger {self.app_name} started up and listening for messages.")
|
|
129
|
+
|
|
130
|
+
def shut_down(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Shuts down the application by stopping the background event loop
|
|
133
|
+
and disconnecting from the message broker.
|
|
134
|
+
"""
|
|
135
|
+
# Close the log file if it's open
|
|
136
|
+
self._close_log_file()
|
|
137
|
+
|
|
138
|
+
# Shut down base application
|
|
139
|
+
super().shut_down()
|
|
140
|
+
|
|
141
|
+
def _open_log_file(self) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Opens a new log file for writing messages.
|
|
144
|
+
"""
|
|
145
|
+
if self.log_file is not None:
|
|
146
|
+
self._close_log_file()
|
|
147
|
+
|
|
148
|
+
ts = (
|
|
149
|
+
str(self.simulator.get_wallclock_time()).replace(" ", "T").replace(":", "-")
|
|
150
|
+
)
|
|
151
|
+
log_filename = os.path.join(self.log_dir, f"{ts}.log")
|
|
152
|
+
self.log_file = open(log_filename, "a")
|
|
153
|
+
self.log_file.write(f"Timestamp,Topic,Payload\n")
|
|
154
|
+
logger.info(f"Logger {self.app_name} opened file {self.log_file.name}.")
|
|
155
|
+
|
|
156
|
+
def _close_log_file(self) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Closes the current log file if it's open.
|
|
159
|
+
"""
|
|
160
|
+
if self.log_file is not None:
|
|
161
|
+
self.log_file.close()
|
|
162
|
+
logger.info(f"Logger {self.app_name} closed file {self.log_file.name}.")
|
|
163
|
+
self.log_file = None
|
|
164
|
+
|
|
165
|
+
def on_log_message(self, ch, method, properties, body):
|
|
166
|
+
"""
|
|
167
|
+
Callback function to log a message received by the logger application.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
ch: The channel object
|
|
171
|
+
method: The method frame
|
|
172
|
+
properties: The message properties
|
|
173
|
+
body: The message body
|
|
174
|
+
"""
|
|
175
|
+
if self.log_file is not None:
|
|
176
|
+
try:
|
|
177
|
+
routing_key = method.routing_key
|
|
178
|
+
payload = body.decode("utf-8") if isinstance(body, bytes) else str(body)
|
|
179
|
+
|
|
180
|
+
logger.debug(f"Logger {self.app_name} logging message: {payload}")
|
|
181
|
+
|
|
182
|
+
timestamp = self.simulator.get_wallclock_time()
|
|
183
|
+
self.log_file.write(f"{timestamp},{routing_key},{payload}\n")
|
|
184
|
+
self.log_file.flush() # Ensure data is written immediately
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error logging message: {e}")
|
|
187
|
+
else:
|
|
188
|
+
# If log file isn't open, try to reopen it
|
|
189
|
+
self._open_log_file()
|
|
190
|
+
logger.error(f"Logger {self.app_name} had to reopen log file.")
|
|
191
|
+
# Try to log the message again
|
|
192
|
+
self.on_log_message(ch, method, properties, body)
|