openrvdas.server.in_memory_server_api
API implementation for interacting with an in-memory data store.
See server/server_api_command_line.py for a sample script that exercises this class. Also see server/server_api.py for full documentation on the ServerAPI.
1#!/usr/bin/env python3 2"""API implementation for interacting with an in-memory data store. 3 4See server/server_api_command_line.py for a sample script that 5exercises this class. Also see server/server_api.py for full 6documentation on the ServerAPI. 7""" 8 9import datetime 10import logging 11import pprint 12import sys 13import time 14 15 16from server.server_api import ServerAPI # noqa: E402 17 18DEFAULT_MAX_TRIES = 3 19ID_SEPARATOR = ':' 20 21################################################################################ 22 23 24class InMemoryServerAPI(ServerAPI): 25 ############################ 26 def __init__(self): 27 super().__init__() 28 self.config = {} 29 self.mode = "n/a" 30 self.logger_config = {} 31 self.callbacks = [] 32 self.status = [] 33 self.server_messages = [] 34 35 ############################# 36 # API methods below are used in querying/modifying the API for the 37 # record of the running state of loggers. 38 ############################ 39 # def get_cruises(self): 40 # """Return list of cruise id's.""" 41 # return list(self.cruise_configs) 42 43 ############################ 44 def get_configuration(self): 45 """Return cruise config for specified cruise id.""" 46 return self.config or None 47 48 ############################ 49 def get_modes(self): 50 """Return list of modes defined for given cruise.""" 51 return list(self.config.get('modes', [])) 52 53 ############################ 54 def get_active_mode(self): 55 """Return cruise config for specified cruise id.""" 56 return self.config.get('active_mode') 57 58 ############################ 59 def get_default_mode(self): 60 """Get the name of the default mode for the specified cruise 61 from the data store.""" 62 return self.config.get('default_mode') 63 64 ############################ 65 def get_logger(self, logger): 66 """Retrieve the logger spec for the specified logger id.""" 67 loggers = self.get_loggers() 68 if logger not in loggers: 69 raise ValueError('No logger "%s" found' % 70 (logger)) 71 return loggers.get(logger) 72 73 ############################ 74 def get_loggers(self): 75 """Get a dict of 76 {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...} 77 for all loggers. 78 """ 79 config = self.get_configuration() 80 if not config: 81 return {} 82 83 if 'loggers' not in config: 84 raise ValueError('No loggers found') 85 logger_configs = config.get('loggers') 86 if not logger_configs: 87 raise ValueError('No logger configurations found') 88 89 # Fetch and insert the currently active config for each logger 90 for logger in logger_configs: 91 logger_configs[logger]['active'] = self.get_logger_config_name(logger) 92 return logger_configs 93 94 ############################ 95 def get_logger_config(self, config_name): 96 """Retrieve the config associated with the specified name.""" 97 cruise_config = self.get_configuration() 98 if cruise_config is None: 99 return {} 100 logger_configs = cruise_config.get('configs') 101 if logger_configs is None: 102 raise ValueError('No "config" found') 103 logger_config = logger_configs.get(config_name) 104 if logger_config is None: 105 raise ValueError('No logger config "%s" in config' % config_name) 106 return logger_config 107 108 ############################ 109 def get_logger_configs(self, mode=None): 110 """Retrieve the configs associated with a cruise id and mode from the 111 data store. If mode is omitted, retrieve configs associated with 112 the cruise's current logger configs.""" 113 loggers = self.get_loggers() 114 if not loggers: 115 return None 116 117 output = {} 118 for logger in loggers: 119 logger_config_name = self.get_logger_config_name(logger, mode) 120 output[logger] = self.get_logger_config(logger_config_name) 121 122 return output 123 124 ############################ 125 def get_logger_config_name(self, logger_id, mode=None): 126 """Retrieve name of the config associated with the specified logger 127 in the specified mode. If mode is omitted, retrieve name of logger's 128 current config.""" 129 130 if not self.config: 131 raise ValueError('No configuration loaded') 132 133 # If mode is not specified, get logger's current config name 134 if mode is None: 135 config_name = self.logger_config.get(logger_id) 136 if config_name is None: 137 raise ValueError(f'Logger id "{logger_id}" has no mode!') 138 return config_name 139 140 # Otherwise, we return the config_name for the specifed mode 141 modes = self.config.get('modes') 142 mode_configs = modes.get(mode) 143 if mode_configs is None: 144 raise ValueError('Requested mode %s is not defined (modes are: %s)' % 145 (mode, [m for m in modes])) 146 logger_config_name = mode_configs.get(logger_id) 147 if logger_config_name is None: 148 raise ValueError('Logger %s has no config defined in mode %s' % 149 (logger_id, mode)) 150 return logger_config_name 151 152 ############################# 153 def get_logger_config_names(self, logger_id): 154 """Retrieve list of config names that are valid for the specified logger . 155 > api.get_logger_config_names('NBP1406', 'knud') 156 ["off", "knud->net", "knud->net/file", "knud->net/file/db"] 157 """ 158 logger = self.get_logger(logger_id) 159 return logger.get('configs', []) 160 161 ############################ 162 # Methods for manipulating the desired state via API to indicate 163 # current mode and which loggers should be in which configs. 164 ############################ 165 def set_active_mode(self, mode): 166 """Set the current mode of the specified cruise in the data store.""" 167 modes = self.config.get('modes') 168 if not modes: 169 raise ValueError('Config has no modes??') 170 if mode not in modes: 171 raise ValueError('Config has no mode "%s"' % (mode)) 172 173 self.config['active_mode'] = mode 174 175 # Update the stored {logger:config_name} dict to match new mode 176 # Here's a quick one-liner that doesn't do any checking: 177 self.logger_config = modes[mode].copy() 178 179 # Here's s slow carefully-checked way of setting configs: 180 # for logger, config in modes[mode].items(): 181 # self.set_logger_config_name(cruise_id, logger, config) 182 183 # Q: At this point should we could signal an update. Or we could 184 # count on the API calling signal_update(). Or count on the update 185 # being picked up by polling. For now, don't signal the update. 186 187 logging.info('Signaling update') 188 self.signal_update() 189 190 ############################ 191 def set_active_logger_config(self, logger, config_name): 192 """Set specified logger to new config. NOTE: we have no way to check 193 whether logger is compatible with config, so we rely on whoever is 194 calling us to have made that determination.""" 195 # if not cruise_id in self.logger_config: 196 # self.logger_config[cruise_id] = {} 197 self.logger_config[logger] = config_name 198 199 logging.info('Signaling update') 200 self.signal_update() 201 202 ############################ 203 # Methods for feeding data from LoggerServer back into the API 204 ############################ 205 def update_status(self, status): 206 """Save/register the loggers' retrieved status report with the API.""" 207 self.status.append((time.time(), status)) 208 209 ############################ 210 # Methods for getting status data from API 211 ############################ 212 213 def get_status(self, since_timestamp=None): 214 """Retrieve a dict of the most-recent status report from each 215 logger. If since_timestamp is specified, retrieve all status reports 216 since that time.""" 217 218 # Start by getting set of loggers for cruise. Store as 219 # cruise_id:logger for ease of lookup. 220 try: 221 logger_set = set([logger 222 for logger in self.get_loggers()]) 223 except ValueError: 224 logger_set = set() 225 226 logging.debug('logger_set: %s', logger_set) 227 228 # Step backwards through status messages until we run out of 229 # status messages or reach termination condition. If 230 # since_timestamp==None, our termination is when we have a status 231 # for each of our loggers. If since_timestamp is a number, our 232 # termination is when we've grabbed all the statuses with a 233 # timestamp greater than the specified number. 234 status = {} 235 236 status_index = len(self.status) - 1 237 logging.debug('starting at status index %d', status_index) 238 while logger_set and status_index >= 0: 239 # record is a dict of 'cruise_id:logger' : {fields} 240 (timestamp, record) = self.status[status_index] 241 logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record)) 242 243 # If we've been given a numeric timestamp and we've stepped back 244 # in time to or before that timestamp, we're done - break out. 245 if since_timestamp is not None and timestamp <= since_timestamp: 246 break 247 248 # Otherwise, examine ids in this record to see if they're for 249 # the cruise in question. 250 for id, fields in record.items(): 251 # If id is cruise_id:logger that we're interested in, grab it. 252 logging.debug('Is %s in %s?', id, logger_set) 253 if id in logger_set: 254 if timestamp not in status: 255 status[timestamp] = {} 256 status[timestamp][id] = fields 257 258 # If since_timestamp==None, we only want the latest status 259 # for each logger. So once we've found it, remove the id 260 # from the logger_set we're lookings. We'll drop out of the 261 # loop when the set is empty. 262 if since_timestamp is None: 263 logger_set.discard(id) 264 status_index -= 1 265 266 return status 267 268 ############################ 269 # Methods for storing/retrieving messages from servers/loggers/etc. 270 ############################ 271 def message_log(self, source, user, log_level, message): 272 """Timestamp and store the passed message.""" 273 self.server_messages.append((time.time(), source, user, 274 log_level, message)) 275 276 ############################ 277 def get_message_log(self, source=None, user=None, log_level=sys.maxsize, 278 since_timestamp=None): 279 """Retrieve log messages from source at or above log_level since 280 timestamp. If source is omitted, retrieve from all sources. If 281 log_level is omitted, retrieve at all levels. If since_timestamp is 282 omitted, only retrieve most recent message. 283 """ 284 index = len(self.server_messages) - 1 285 messages = [] 286 while index >= 0: 287 message = self.server_messages[index] 288 (timestamp, mesg_source, mesg_user, 289 mesg_log_level, mesg_message) = message 290 # Have we gone back too far? If so, we're done. 291 if since_timestamp is not None and timestamp <= since_timestamp: 292 break 293 294 if mesg_log_level < log_level: 295 continue 296 if user and not mesg_user == user: 297 continue 298 if source and not mesg_source == source: 299 continue 300 301 messages.insert(0, message) 302 303 # Are we only looking for last message, and do we have a message? 304 if since_timestamp is None and messages: 305 break 306 index -= 1 307 308 return messages 309 310 ############################# 311 # Methods to modify the data store 312 ############################ 313 def load_configuration(self, config): 314 """Add a complete cruise configuration (id, modes, configs, 315 default) to the data store.""" 316 self.config = config 317 self.config['loaded_time'] = datetime.datetime.utcnow() 318 319 # Some syntactic sugar to simplify config definitions 320 configs = self.config.get('configs') 321 for config_name, config in configs.items(): 322 if config is None: 323 raise ValueError(f'No logger for "{config_name}" in cruise definition') 324 if 'name' not in config: 325 self.config['configs'][config_name]['name'] = config_name 326 327 # Set cruise into default mode, if one is defined 328 if 'default_mode' in self.config: 329 active_mode = self.config['default_mode'] 330 self.set_active_mode(active_mode) 331 332 # Let anyone who's interested know that we've got new configurations. 333 self.signal_load() 334 335 ############################ 336 def delete_configuration(self): 337 """Remove the specified cruise from the data store.""" 338 self.config = {} 339 self.mode = "n/a" 340 self.logger_config = {} 341 self.callbacks = [] 342 self.status = [] 343 344 ############################ 345 # Methods for manually constructing/modifying a cruise spec via API 346 # def add_cruise(self, cruise_id, start=None, end=None) 347 # def add_mode(self, cruise_id, mode) 348 # def delete_mode(self, cruise_id, mode) 349 # def add_logger(self, cruise_id, logger_id, logger_spec) 350 # def delete_logger(self, cruise_id, logger_id) 351 # def add_config(self, cruise_id, config, config_spec) 352 # def add_config_to_logger(self, cruise_id, config, logger_id) 353 # def add_config_to_mode(self, cruise_id, config, logger_id, mode) 354 # def delete_config(self, cruise_id, config_id)
25class InMemoryServerAPI(ServerAPI): 26 ############################ 27 def __init__(self): 28 super().__init__() 29 self.config = {} 30 self.mode = "n/a" 31 self.logger_config = {} 32 self.callbacks = [] 33 self.status = [] 34 self.server_messages = [] 35 36 ############################# 37 # API methods below are used in querying/modifying the API for the 38 # record of the running state of loggers. 39 ############################ 40 # def get_cruises(self): 41 # """Return list of cruise id's.""" 42 # return list(self.cruise_configs) 43 44 ############################ 45 def get_configuration(self): 46 """Return cruise config for specified cruise id.""" 47 return self.config or None 48 49 ############################ 50 def get_modes(self): 51 """Return list of modes defined for given cruise.""" 52 return list(self.config.get('modes', [])) 53 54 ############################ 55 def get_active_mode(self): 56 """Return cruise config for specified cruise id.""" 57 return self.config.get('active_mode') 58 59 ############################ 60 def get_default_mode(self): 61 """Get the name of the default mode for the specified cruise 62 from the data store.""" 63 return self.config.get('default_mode') 64 65 ############################ 66 def get_logger(self, logger): 67 """Retrieve the logger spec for the specified logger id.""" 68 loggers = self.get_loggers() 69 if logger not in loggers: 70 raise ValueError('No logger "%s" found' % 71 (logger)) 72 return loggers.get(logger) 73 74 ############################ 75 def get_loggers(self): 76 """Get a dict of 77 {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...} 78 for all loggers. 79 """ 80 config = self.get_configuration() 81 if not config: 82 return {} 83 84 if 'loggers' not in config: 85 raise ValueError('No loggers found') 86 logger_configs = config.get('loggers') 87 if not logger_configs: 88 raise ValueError('No logger configurations found') 89 90 # Fetch and insert the currently active config for each logger 91 for logger in logger_configs: 92 logger_configs[logger]['active'] = self.get_logger_config_name(logger) 93 return logger_configs 94 95 ############################ 96 def get_logger_config(self, config_name): 97 """Retrieve the config associated with the specified name.""" 98 cruise_config = self.get_configuration() 99 if cruise_config is None: 100 return {} 101 logger_configs = cruise_config.get('configs') 102 if logger_configs is None: 103 raise ValueError('No "config" found') 104 logger_config = logger_configs.get(config_name) 105 if logger_config is None: 106 raise ValueError('No logger config "%s" in config' % config_name) 107 return logger_config 108 109 ############################ 110 def get_logger_configs(self, mode=None): 111 """Retrieve the configs associated with a cruise id and mode from the 112 data store. If mode is omitted, retrieve configs associated with 113 the cruise's current logger configs.""" 114 loggers = self.get_loggers() 115 if not loggers: 116 return None 117 118 output = {} 119 for logger in loggers: 120 logger_config_name = self.get_logger_config_name(logger, mode) 121 output[logger] = self.get_logger_config(logger_config_name) 122 123 return output 124 125 ############################ 126 def get_logger_config_name(self, logger_id, mode=None): 127 """Retrieve name of the config associated with the specified logger 128 in the specified mode. If mode is omitted, retrieve name of logger's 129 current config.""" 130 131 if not self.config: 132 raise ValueError('No configuration loaded') 133 134 # If mode is not specified, get logger's current config name 135 if mode is None: 136 config_name = self.logger_config.get(logger_id) 137 if config_name is None: 138 raise ValueError(f'Logger id "{logger_id}" has no mode!') 139 return config_name 140 141 # Otherwise, we return the config_name for the specifed mode 142 modes = self.config.get('modes') 143 mode_configs = modes.get(mode) 144 if mode_configs is None: 145 raise ValueError('Requested mode %s is not defined (modes are: %s)' % 146 (mode, [m for m in modes])) 147 logger_config_name = mode_configs.get(logger_id) 148 if logger_config_name is None: 149 raise ValueError('Logger %s has no config defined in mode %s' % 150 (logger_id, mode)) 151 return logger_config_name 152 153 ############################# 154 def get_logger_config_names(self, logger_id): 155 """Retrieve list of config names that are valid for the specified logger . 156 > api.get_logger_config_names('NBP1406', 'knud') 157 ["off", "knud->net", "knud->net/file", "knud->net/file/db"] 158 """ 159 logger = self.get_logger(logger_id) 160 return logger.get('configs', []) 161 162 ############################ 163 # Methods for manipulating the desired state via API to indicate 164 # current mode and which loggers should be in which configs. 165 ############################ 166 def set_active_mode(self, mode): 167 """Set the current mode of the specified cruise in the data store.""" 168 modes = self.config.get('modes') 169 if not modes: 170 raise ValueError('Config has no modes??') 171 if mode not in modes: 172 raise ValueError('Config has no mode "%s"' % (mode)) 173 174 self.config['active_mode'] = mode 175 176 # Update the stored {logger:config_name} dict to match new mode 177 # Here's a quick one-liner that doesn't do any checking: 178 self.logger_config = modes[mode].copy() 179 180 # Here's s slow carefully-checked way of setting configs: 181 # for logger, config in modes[mode].items(): 182 # self.set_logger_config_name(cruise_id, logger, config) 183 184 # Q: At this point should we could signal an update. Or we could 185 # count on the API calling signal_update(). Or count on the update 186 # being picked up by polling. For now, don't signal the update. 187 188 logging.info('Signaling update') 189 self.signal_update() 190 191 ############################ 192 def set_active_logger_config(self, logger, config_name): 193 """Set specified logger to new config. NOTE: we have no way to check 194 whether logger is compatible with config, so we rely on whoever is 195 calling us to have made that determination.""" 196 # if not cruise_id in self.logger_config: 197 # self.logger_config[cruise_id] = {} 198 self.logger_config[logger] = config_name 199 200 logging.info('Signaling update') 201 self.signal_update() 202 203 ############################ 204 # Methods for feeding data from LoggerServer back into the API 205 ############################ 206 def update_status(self, status): 207 """Save/register the loggers' retrieved status report with the API.""" 208 self.status.append((time.time(), status)) 209 210 ############################ 211 # Methods for getting status data from API 212 ############################ 213 214 def get_status(self, since_timestamp=None): 215 """Retrieve a dict of the most-recent status report from each 216 logger. If since_timestamp is specified, retrieve all status reports 217 since that time.""" 218 219 # Start by getting set of loggers for cruise. Store as 220 # cruise_id:logger for ease of lookup. 221 try: 222 logger_set = set([logger 223 for logger in self.get_loggers()]) 224 except ValueError: 225 logger_set = set() 226 227 logging.debug('logger_set: %s', logger_set) 228 229 # Step backwards through status messages until we run out of 230 # status messages or reach termination condition. If 231 # since_timestamp==None, our termination is when we have a status 232 # for each of our loggers. If since_timestamp is a number, our 233 # termination is when we've grabbed all the statuses with a 234 # timestamp greater than the specified number. 235 status = {} 236 237 status_index = len(self.status) - 1 238 logging.debug('starting at status index %d', status_index) 239 while logger_set and status_index >= 0: 240 # record is a dict of 'cruise_id:logger' : {fields} 241 (timestamp, record) = self.status[status_index] 242 logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record)) 243 244 # If we've been given a numeric timestamp and we've stepped back 245 # in time to or before that timestamp, we're done - break out. 246 if since_timestamp is not None and timestamp <= since_timestamp: 247 break 248 249 # Otherwise, examine ids in this record to see if they're for 250 # the cruise in question. 251 for id, fields in record.items(): 252 # If id is cruise_id:logger that we're interested in, grab it. 253 logging.debug('Is %s in %s?', id, logger_set) 254 if id in logger_set: 255 if timestamp not in status: 256 status[timestamp] = {} 257 status[timestamp][id] = fields 258 259 # If since_timestamp==None, we only want the latest status 260 # for each logger. So once we've found it, remove the id 261 # from the logger_set we're lookings. We'll drop out of the 262 # loop when the set is empty. 263 if since_timestamp is None: 264 logger_set.discard(id) 265 status_index -= 1 266 267 return status 268 269 ############################ 270 # Methods for storing/retrieving messages from servers/loggers/etc. 271 ############################ 272 def message_log(self, source, user, log_level, message): 273 """Timestamp and store the passed message.""" 274 self.server_messages.append((time.time(), source, user, 275 log_level, message)) 276 277 ############################ 278 def get_message_log(self, source=None, user=None, log_level=sys.maxsize, 279 since_timestamp=None): 280 """Retrieve log messages from source at or above log_level since 281 timestamp. If source is omitted, retrieve from all sources. If 282 log_level is omitted, retrieve at all levels. If since_timestamp is 283 omitted, only retrieve most recent message. 284 """ 285 index = len(self.server_messages) - 1 286 messages = [] 287 while index >= 0: 288 message = self.server_messages[index] 289 (timestamp, mesg_source, mesg_user, 290 mesg_log_level, mesg_message) = message 291 # Have we gone back too far? If so, we're done. 292 if since_timestamp is not None and timestamp <= since_timestamp: 293 break 294 295 if mesg_log_level < log_level: 296 continue 297 if user and not mesg_user == user: 298 continue 299 if source and not mesg_source == source: 300 continue 301 302 messages.insert(0, message) 303 304 # Are we only looking for last message, and do we have a message? 305 if since_timestamp is None and messages: 306 break 307 index -= 1 308 309 return messages 310 311 ############################# 312 # Methods to modify the data store 313 ############################ 314 def load_configuration(self, config): 315 """Add a complete cruise configuration (id, modes, configs, 316 default) to the data store.""" 317 self.config = config 318 self.config['loaded_time'] = datetime.datetime.utcnow() 319 320 # Some syntactic sugar to simplify config definitions 321 configs = self.config.get('configs') 322 for config_name, config in configs.items(): 323 if config is None: 324 raise ValueError(f'No logger for "{config_name}" in cruise definition') 325 if 'name' not in config: 326 self.config['configs'][config_name]['name'] = config_name 327 328 # Set cruise into default mode, if one is defined 329 if 'default_mode' in self.config: 330 active_mode = self.config['default_mode'] 331 self.set_active_mode(active_mode) 332 333 # Let anyone who's interested know that we've got new configurations. 334 self.signal_load() 335 336 ############################ 337 def delete_configuration(self): 338 """Remove the specified cruise from the data store.""" 339 self.config = {} 340 self.mode = "n/a" 341 self.logger_config = {} 342 self.callbacks = [] 343 self.status = [] 344 345 ############################ 346 # Methods for manually constructing/modifying a cruise spec via API 347 # def add_cruise(self, cruise_id, start=None, end=None) 348 # def add_mode(self, cruise_id, mode) 349 # def delete_mode(self, cruise_id, mode) 350 # def add_logger(self, cruise_id, logger_id, logger_spec) 351 # def delete_logger(self, cruise_id, logger_id) 352 # def add_config(self, cruise_id, config, config_spec) 353 # def add_config_to_logger(self, cruise_id, config, logger_id) 354 # def add_config_to_mode(self, cruise_id, config, logger_id, mode) 355 # def delete_config(self, cruise_id, config_id)
Abstract base class defining an API through which a LoggerServer can interact with a data store.
Parameters below have the following semantics:
configuration - dict definition of a OpenRVDAS configuration
mode - dict name of mode, and logger_config_names associated with that mode
default_mode - name of mode to use at startup or when returning to a default state
logger_config_name - string name of a logger configuration, unique within configration
logger_config - dict definition of a logger configuration
logger_id - string name of logger, unique within cruise
logger - dict definition of logger, including list of names of
valid configs and optional host restriction
logger_configs - dict of {logger_config_name:logger_config,...}
For the purposes of documentation below, assume a sample cruise_config as follows:
{
"loggers": {
"knud": {
"host": "knud.pi",
"configs": ["off", "knud->net", "knud->file/net/db"]
},
"gyr1": {
"configs": ["off", "gyr1->net", "gyr1->file/net/db"]
},
"modes": {
"off": {"knud": "off", "gyr1": "off"},
"port": {"knud": "off", "gyr1": "gyr1->net"},
"underway": { "knud": "knud->file/net/db",
"gyr1": "gyr1->file/net/db"
}
},
"default_mode": "off",
"configs": {
"off": {},
"knud->net": { config_spec },
"knud->file/net/db": { config_spec },
"gyr1->net": { config_spec },
"gyr1->file/net/dbnet": { config_spec }
}
}
45 def get_configuration(self): 46 """Return cruise config for specified cruise id.""" 47 return self.config or None
Return cruise config for specified cruise id.
50 def get_modes(self): 51 """Return list of modes defined for given cruise.""" 52 return list(self.config.get('modes', []))
Return list of modes defined for given cruise.
55 def get_active_mode(self): 56 """Return cruise config for specified cruise id.""" 57 return self.config.get('active_mode')
Return cruise config for specified cruise id.
60 def get_default_mode(self): 61 """Get the name of the default mode for the specified cruise 62 from the data store.""" 63 return self.config.get('default_mode')
Get the name of the default mode for the specified cruise from the data store.
66 def get_logger(self, logger): 67 """Retrieve the logger spec for the specified logger id.""" 68 loggers = self.get_loggers() 69 if logger not in loggers: 70 raise ValueError('No logger "%s" found' % 71 (logger)) 72 return loggers.get(logger)
Retrieve the logger spec for the specified logger id.
75 def get_loggers(self): 76 """Get a dict of 77 {logger_id:{'configs':[<name_1>,<name_2>,...], 'active':<name>},...} 78 for all loggers. 79 """ 80 config = self.get_configuration() 81 if not config: 82 return {} 83 84 if 'loggers' not in config: 85 raise ValueError('No loggers found') 86 logger_configs = config.get('loggers') 87 if not logger_configs: 88 raise ValueError('No logger configurations found') 89 90 # Fetch and insert the currently active config for each logger 91 for logger in logger_configs: 92 logger_configs[logger]['active'] = self.get_logger_config_name(logger) 93 return logger_configs
Get a dict of
{logger_id:{'configs':[
96 def get_logger_config(self, config_name): 97 """Retrieve the config associated with the specified name.""" 98 cruise_config = self.get_configuration() 99 if cruise_config is None: 100 return {} 101 logger_configs = cruise_config.get('configs') 102 if logger_configs is None: 103 raise ValueError('No "config" found') 104 logger_config = logger_configs.get(config_name) 105 if logger_config is None: 106 raise ValueError('No logger config "%s" in config' % config_name) 107 return logger_config
Retrieve the config associated with the specified name.
110 def get_logger_configs(self, mode=None): 111 """Retrieve the configs associated with a cruise id and mode from the 112 data store. If mode is omitted, retrieve configs associated with 113 the cruise's current logger configs.""" 114 loggers = self.get_loggers() 115 if not loggers: 116 return None 117 118 output = {} 119 for logger in loggers: 120 logger_config_name = self.get_logger_config_name(logger, mode) 121 output[logger] = self.get_logger_config(logger_config_name) 122 123 return output
Retrieve the configs associated with a cruise id and mode from the data store. If mode is omitted, retrieve configs associated with the cruise's current logger configs.
126 def get_logger_config_name(self, logger_id, mode=None): 127 """Retrieve name of the config associated with the specified logger 128 in the specified mode. If mode is omitted, retrieve name of logger's 129 current config.""" 130 131 if not self.config: 132 raise ValueError('No configuration loaded') 133 134 # If mode is not specified, get logger's current config name 135 if mode is None: 136 config_name = self.logger_config.get(logger_id) 137 if config_name is None: 138 raise ValueError(f'Logger id "{logger_id}" has no mode!') 139 return config_name 140 141 # Otherwise, we return the config_name for the specifed mode 142 modes = self.config.get('modes') 143 mode_configs = modes.get(mode) 144 if mode_configs is None: 145 raise ValueError('Requested mode %s is not defined (modes are: %s)' % 146 (mode, [m for m in modes])) 147 logger_config_name = mode_configs.get(logger_id) 148 if logger_config_name is None: 149 raise ValueError('Logger %s has no config defined in mode %s' % 150 (logger_id, mode)) 151 return logger_config_name
Retrieve name of the config associated with the specified logger in the specified mode. If mode is omitted, retrieve name of logger's current config.
154 def get_logger_config_names(self, logger_id): 155 """Retrieve list of config names that are valid for the specified logger . 156 > api.get_logger_config_names('NBP1406', 'knud') 157 ["off", "knud->net", "knud->net/file", "knud->net/file/db"] 158 """ 159 logger = self.get_logger(logger_id) 160 return logger.get('configs', [])
Retrieve list of config names that are valid for the specified logger .
api.get_logger_config_names('NBP1406', 'knud') ["off", "knud->net", "knud->net/file", "knud->net/file/db"]
166 def set_active_mode(self, mode): 167 """Set the current mode of the specified cruise in the data store.""" 168 modes = self.config.get('modes') 169 if not modes: 170 raise ValueError('Config has no modes??') 171 if mode not in modes: 172 raise ValueError('Config has no mode "%s"' % (mode)) 173 174 self.config['active_mode'] = mode 175 176 # Update the stored {logger:config_name} dict to match new mode 177 # Here's a quick one-liner that doesn't do any checking: 178 self.logger_config = modes[mode].copy() 179 180 # Here's s slow carefully-checked way of setting configs: 181 # for logger, config in modes[mode].items(): 182 # self.set_logger_config_name(cruise_id, logger, config) 183 184 # Q: At this point should we could signal an update. Or we could 185 # count on the API calling signal_update(). Or count on the update 186 # being picked up by polling. For now, don't signal the update. 187 188 logging.info('Signaling update') 189 self.signal_update()
Set the current mode of the specified cruise in the data store.
192 def set_active_logger_config(self, logger, config_name): 193 """Set specified logger to new config. NOTE: we have no way to check 194 whether logger is compatible with config, so we rely on whoever is 195 calling us to have made that determination.""" 196 # if not cruise_id in self.logger_config: 197 # self.logger_config[cruise_id] = {} 198 self.logger_config[logger] = config_name 199 200 logging.info('Signaling update') 201 self.signal_update()
Set specified logger to new config. NOTE: we have no way to check whether logger is compatible with config, so we rely on whoever is calling us to have made that determination.
206 def update_status(self, status): 207 """Save/register the loggers' retrieved status report with the API.""" 208 self.status.append((time.time(), status))
Save/register the loggers' retrieved status report with the API.
214 def get_status(self, since_timestamp=None): 215 """Retrieve a dict of the most-recent status report from each 216 logger. If since_timestamp is specified, retrieve all status reports 217 since that time.""" 218 219 # Start by getting set of loggers for cruise. Store as 220 # cruise_id:logger for ease of lookup. 221 try: 222 logger_set = set([logger 223 for logger in self.get_loggers()]) 224 except ValueError: 225 logger_set = set() 226 227 logging.debug('logger_set: %s', logger_set) 228 229 # Step backwards through status messages until we run out of 230 # status messages or reach termination condition. If 231 # since_timestamp==None, our termination is when we have a status 232 # for each of our loggers. If since_timestamp is a number, our 233 # termination is when we've grabbed all the statuses with a 234 # timestamp greater than the specified number. 235 status = {} 236 237 status_index = len(self.status) - 1 238 logging.debug('starting at status index %d', status_index) 239 while logger_set and status_index >= 0: 240 # record is a dict of 'cruise_id:logger' : {fields} 241 (timestamp, record) = self.status[status_index] 242 logging.debug('%d: %f: %s', status_index, timestamp, pprint.pformat(record)) 243 244 # If we've been given a numeric timestamp and we've stepped back 245 # in time to or before that timestamp, we're done - break out. 246 if since_timestamp is not None and timestamp <= since_timestamp: 247 break 248 249 # Otherwise, examine ids in this record to see if they're for 250 # the cruise in question. 251 for id, fields in record.items(): 252 # If id is cruise_id:logger that we're interested in, grab it. 253 logging.debug('Is %s in %s?', id, logger_set) 254 if id in logger_set: 255 if timestamp not in status: 256 status[timestamp] = {} 257 status[timestamp][id] = fields 258 259 # If since_timestamp==None, we only want the latest status 260 # for each logger. So once we've found it, remove the id 261 # from the logger_set we're lookings. We'll drop out of the 262 # loop when the set is empty. 263 if since_timestamp is None: 264 logger_set.discard(id) 265 status_index -= 1 266 267 return status
Retrieve a dict of the most-recent status report from each logger. If since_timestamp is specified, retrieve all status reports since that time.
272 def message_log(self, source, user, log_level, message): 273 """Timestamp and store the passed message.""" 274 self.server_messages.append((time.time(), source, user, 275 log_level, message))
Timestamp and store the passed message.
278 def get_message_log(self, source=None, user=None, log_level=sys.maxsize, 279 since_timestamp=None): 280 """Retrieve log messages from source at or above log_level since 281 timestamp. If source is omitted, retrieve from all sources. If 282 log_level is omitted, retrieve at all levels. If since_timestamp is 283 omitted, only retrieve most recent message. 284 """ 285 index = len(self.server_messages) - 1 286 messages = [] 287 while index >= 0: 288 message = self.server_messages[index] 289 (timestamp, mesg_source, mesg_user, 290 mesg_log_level, mesg_message) = message 291 # Have we gone back too far? If so, we're done. 292 if since_timestamp is not None and timestamp <= since_timestamp: 293 break 294 295 if mesg_log_level < log_level: 296 continue 297 if user and not mesg_user == user: 298 continue 299 if source and not mesg_source == source: 300 continue 301 302 messages.insert(0, message) 303 304 # Are we only looking for last message, and do we have a message? 305 if since_timestamp is None and messages: 306 break 307 index -= 1 308 309 return messages
Retrieve log messages from source at or above log_level since timestamp. If source is omitted, retrieve from all sources. If log_level is omitted, retrieve at all levels. If since_timestamp is omitted, only retrieve most recent message.
314 def load_configuration(self, config): 315 """Add a complete cruise configuration (id, modes, configs, 316 default) to the data store.""" 317 self.config = config 318 self.config['loaded_time'] = datetime.datetime.utcnow() 319 320 # Some syntactic sugar to simplify config definitions 321 configs = self.config.get('configs') 322 for config_name, config in configs.items(): 323 if config is None: 324 raise ValueError(f'No logger for "{config_name}" in cruise definition') 325 if 'name' not in config: 326 self.config['configs'][config_name]['name'] = config_name 327 328 # Set cruise into default mode, if one is defined 329 if 'default_mode' in self.config: 330 active_mode = self.config['default_mode'] 331 self.set_active_mode(active_mode) 332 333 # Let anyone who's interested know that we've got new configurations. 334 self.signal_load()
Add a complete cruise configuration (id, modes, configs, default) to the data store.