In this post, we will build a data structure to hold Cisco IOS XE logs. Before we begin, let's examine a few log messages and identify the parts of a log message. These parts will become the fields in our data structure / class.
*Oct 30 05:26:10.166: %VUDI-6-EVENT: [serial number: 9ZK2PRA86LM], [vUDI: ], vUDI is successfully retrieved from license file
*Oct 30 05:26:10.227: %IOS_LICENSE_IMAGE_APPLICATION-6-LICENSE_LEVEL: Module name = csr1000v Next reboot level = ax and License = No valid license found
*Oct 30 05:26:12.160: %SMART_LIC-6-AGENT_READY: Smart Agent for Licensing is initialized
*Oct 30 05:26:12.189: %VXE_THROUGHPUT-6-LEVEL: Throughput level has been set to 1000 kbps
*Oct 30 05:26:12.189: %VXE_THROUGHPUT-2-LOW_THROUGHPUT: System throughput set to low default level 1000 kbps, system performance can be severely impacted. Please install a valid license, configure the boot level and reload, to switch to a higher throughput
*Oct 30 05:26:15.221: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet1, changed state to down
*Oct 30 05:26:15.965: %SPANTREE-5-EXTENDED_SYSID: Extended SysId enabled for type vlan
*Oct 30 05:26:16.083: %VOICE_HA-7-STATUS: CUBE HA-supported platform detected.
*Oct 30 05:26:16.289: %LINK-3-UPDOWN: Interface Lsmpi0, changed state to up
*Oct 30 2025 05:25:13.402: %IOSXE-3-PLATFORM: R0/0: kernel: Warning: unable to open an initial console.
*Oct 30 2025 05:25:13.448: %IOSXE-4-PLATFORM: R0/0: kernel: cpldha: loading out-of-tree module taints kernel.
*Oct 30 2025 05:25:13.544: %IOSXE-4-PLATFORM: R0/0: kernel: ACPI: PCI Interrupt Link [LNKC] enabled at IRQ 11
*Oct 30 2025 05:25:52.544: %CPPDRV-4-CPU_FEATURE: R0/0: cpp_driver: CPP0: CPU lacks feature (Population Count HW-Assist (POPCNT)). Performance may be sub-optimal.
*Oct 30 2025 05:25:52.545: %CPPDRV-4-CPU_FEATURE: R0/0: cpp_driver: CPP0: CPU lacks feature (AES HW-Assist (AES-NI)). Performance may be sub-optimal.
*Oct 30 2025 05:25:52.545: %CPPDRV-3-GUEST_CPU_FEATURE: R0/0: cpp_driver: CPP0: Guest CPU lacks feature (Supplemental Streaming SIMD Extensions 3 (SSSE3)).
*Oct 30 2025 05:26:17.048: %SYS-5-CONFIG_I: Configured from memory by console
*Oct 30 2025 05:26:17.092: %SYS-5-RESTART: System restarted --
Cisco IOS Software [Fuji], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.8.1a, RELEASE SOFTWARE (fc1)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2018 by Cisco Systems, Inc.
Compiled Tue 03-Apr-18 18:43 by mcpre
*Oct 30 2025 05:26:17.144: %IOSXE_OIR-6-INSCARD: Card (rp) inserted in slot R1
*Oct 30 2025 05:26:17.323: %SSH-5-ENABLED: SSH 1.99 has been enabled
*Oct 30 2025 05:26:17.328: %LINK-3-UPDOWN: Interface LIIN0, changed state to up
*Oct 30 2025 05:26:17.328: %LINK-5-CHANGED: Interface GigabitEthernet1, changed state to administratively down
*Oct 30 2025 05:26:17.328: %LINEPROTO-5-UPDOWN: Line protocol on Interface Lsmpi0, changed state to up
*Oct 30 2025 05:26:17.329: %LINK-5-CHANGED: Interface GigabitEthernet3, changed state to administratively down
*Oct 30 2025 05:26:17.353: %CRYPTO-6-ISAKMP_ON_OFF: ISAKMP is OFF
*Oct 30 2025 05:26:17.353: %CRYPTO-6-GDOI_ON_OFF: GDOI is OFF
*Oct 30 2025 05:26:17.401: %SYS-6-BOOTTIME: Time taken to reboot after reload = 94 seconds
*Oct 30 2025 05:26:18.327: %LINEPROTO-5-UPDOWN: Line protocol on Interface LIIN0, changed state to up
*Oct 30 2025 05:26:18.327: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet1, changed state to down
*Oct 30 2025 05:26:18.329: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet2, changed state to down
*Oct 30 2025 05:26:18.329: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet3, changed state to down
*Oct 30 2025 05:26:18.351: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet4, changed state to down
*Oct 30 2025 05:26:22.585: %PNP-6-PNP_DISCOVERY_STOPPED: PnP Discovery stopped (Startup Config Present)
Even though there is a lot of diversity in the log messages but they follow this format
<datetime>: %<facility>-<severity>-<mnemonic>: <message_text>
If we look at this message for interface state
*Oct 30 2025 05:26:18.327: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet1, changed state to down
| Field | Example | Description |
|---|---|---|
| datetime | Oct 30 2025 05:26:22.585 |
When the log was generated |
| facility | LINKPROTO |
Subsystem |
| severity | 5 |
Syslog severity level (0–7) |
| mnemonic | UPDOWN |
Event identifier |
| message | Line protocol on Interface GigabitEthernet1, changed state to down |
Log description |
We need to build a software that can analyse logs, and provide reporting.
LogMessage class
We will start by defining a class that can hold data for a log message.
class LogMessage:
def __init__(self, date_time, facility, severity, event_type, message):
self._date_time = date_time
self._facility = facility
self._severity = severity
self._event_type = event_type
self._message = message
@property
def date_time(self):
return self._date_time
@date_time.setter
def date_time(self, value):
self._date_time = value
@property
def facility(self):
return self._facility
@facility.setter
def facility(self, value):
self._facility = value
@property
def severity(self):
return self._severity
@severity.setter
def severity(self, value):
if value < 0 or value > 7:
raise ValueError("Severity ranges from 0 to 7")
self._severity = value
@property
def event_type(self):
return self._event_type
@event_type.setter
def event_type(self, value):
self._event_type = value
@property
def message(self):
return self._message
@message.setter
def message(self, value):
self._message = value
def __str__(self):
return f"{self.date_time}: %{self.facility}-{self.severity}-{self.event_type}: {self.message}"
def old_message(self):
pass
def new_message(self):
pass
def is_critical(self):
return self.severity >= 5
The class structure is simple, it has 5 object properties, one for each field in the log message. We are using private properties in our class, private properties in Python are denoted by underscore and the property name like _severity . We use getters and setters for getting and setting value of each property.
A Getter method is denoted by a property decorator, don't take any parameter and returns the value of property it is created for. A Setter on the other hand takes a parameter and sets the value of property equal to that parameter. Setters can have sanity checking and raise an exception invalid data is passed.
The benefit of using this approach is when we are setting values of a property we can do some basic sanity checks, in our case we are checking if the severity is between 0 and 7, this avoid invalid data and makes our life easier when we do calculations based on that data.
We have three utility methods as well in our class old_message checks if a log message is more than a month old from today's date, new_message checks if the message is less than or equal to 48 hours, finally is_critical method checks if the message has a severity of 5 or more. Notice is_critical method is totally dependant on the severity if have invalid data in that field then is_critical can't work correctly. Its a good idea to have basic checks in setters.
Setters are automatically called even in __init__ method, if you have invalid data for a property and that property has some data checking code, that code will run and if it is designed to raise an error like our setter method for severity, an error will get generated and Object will not be created.
Let's write another class that will be our Operations class and we will extend it as we progress with this project. Initially, it will only have one class method to take a log message, parse it and return a LogMessage object.
Operations class
from datetime import datetime
from base_classes import LogMessage
class Operations:
@classmethod
def parse_log_message(cls, log_message):
if log_message[0] == '*':
log_message = log_message[1:]
message_list = log_message.split(': ')
date_time_str = message_list[0]
date_time = datetime.strptime(date_time_str, "%b %d %Y %H:%M:%S.%f")
facility = message_list[1].split('-')[0][1:]
severity = int(message_list[1].split('-')[1])
event_type = message_list[1].split('-')[2]
if len(message_list) > 3:
message = ' '.join(message_list[2:])
else:
message = message_list[2]
return LogMessage(
date_time=date_time,
facility=facility,
severity=severity,
event_type=event_type,
message=message
)
Our Operations class is simple, it has one class method so that we don't have to create an object of this class to parse log messages. It takes a log message and see if it starts with * which most Cisco messages start with, it does have a * at beginning it removes it and store rest of message to log_message variable.
Then it splits on : - we could use regular expressions here but I have kept it simple and using simple split method on string class. As we know first part of log message is date & time so we store message_list[0] to date_time_str and then convert that string to a date time object as we will need to compare date/times so we need it in a proper format not a string.
Second part of log message is %<facility>-<severity>-<mnemonic> so we take second element of our list message_list[1], split it on - and save it into variables.
Next, we check if the length of our list is greater than 3, it is in the cases when log message has extra information then we join rest of the message into a single message and save it.
Finally, we pass all these values to LogMessage class initializer, create and return an object of LogMessage class.
We have done fair amount of work, in the next post we will add a second method to our Operations class which will take a text file with log messages, parse it using the class method parse_log_message and return a list of LogMessage objects.