Comparing Connection-Log Summaries in Python
Compare two connection logs to find source, destination and port groups that appeared, disappeared or changed count. Two small files will show how the direction of comparison changes the result.
You'll need Python 3, a plain-text editor and a command window. No extra packages are required.
1. Create two input files
Use a tab-separated text file with four columns: time, source address, destination address and destination port. The header must be Time, Source, Destination, Port, with actual tabs between the names. A .tsv file is a plain-text table that uses tabs to separate columns.
Write times as YYYY-MM-DDThh:mm:ssZ. The Z means Coordinated Universal Time (UTC), the shared reference time rather than a local clock. This reader accepts whole seconds in that exact form, not fractional seconds, local offsets such as +01:00 or leap-second values. Addresses use Internet Protocol version 4 (IPv4), four dot-separated numbers such as 192.0.2.10, without extra leading zeros. Ports are whole numbers from 1 to 65535.
The sample addresses are reserved for documentation in RFC 5737, a published document describing addresses to use in examples. Preserve the tabs when copying. This is the reader's own input format, not a direct export from a network-monitoring product.
Save this as before.tsv:
Time Source Destination Port
2026-09-30T09:00:00Z 192.0.2.10 198.51.100.20 443
2026-09-30T09:00:01Z 192.0.2.10 198.51.100.20 443
2026-09-30T09:00:02Z 192.0.2.11 203.0.113.30 53
2026-09-30T09:00:03Z 192.0.2.13 203.0.113.40 25
Then save after.tsv:
Time Source Destination Port
2026-09-30T10:00:00Z 192.0.2.10 198.51.100.20 443
2026-09-30T10:00:01Z 192.0.2.10 198.51.100.20 443
2026-09-30T10:00:02Z 192.0.2.10 198.51.100.20 443
2026-09-30T10:00:03Z 192.0.2.14 203.0.113.50 3389
2026-09-30T10:00:04Z 192.0.2.11 203.0.113.30 53
These files do not declare their collection start and end. We can compare row counts, but cannot calculate rates without knowing the periods they cover.
2. Add the reader
The complete reader is included below so you can run this guide on its own.
Save this as LogReader.py. This reusable source file checks each row and keeps its physical line number, either with accepted values or a reason it could not be read.
from dataclasses import dataclass as DataClass
from datetime import datetime as DateTime
from pathlib import Path
@DataClass
class LogEntry:
Number: int
Original: str
EventTime: str = ""
Source: str = ""
Destination: str = ""
Port: int = 0
Problem: str = ""
Accepted: bool = False
def Digits(Value, Maximum):
if not Value:
raise ValueError("empty number")
if any(Character not in "0123456789" for Character in Value):
raise ValueError("number needs ordinary digits")
Number = int(Value)
if Number > Maximum:
raise ValueError("number outside supported range")
return Number
def CheckAddress(Value):
Parts = Value.split(".")
if len(Parts) != 4:
raise ValueError("expected four IPv4 address parts")
for Part in Parts:
if len(Part) > 3 or (len(Part) > 1 and Part.startswith("0")):
raise ValueError("use canonical dotted IPv4 addresses")
Digits(Part, 255)
def CheckTime(Value):
if len(Value) != 20:
raise ValueError("expected time YYYY-MM-DDThh:mm:ssZ")
if any(Value[Index] != Mark for Index, Mark in
((4, "-"), (7, "-"), (10, "T"), (13, ":"), (16, ":"), (19, "Z"))):
raise ValueError("expected time YYYY-MM-DDThh:mm:ssZ")
NumberText = Value[0:4] + Value[5:7] + Value[8:10] + Value[11:13] + Value[14:16] + Value[17:19]
if any(Character not in "0123456789" for Character in NumberText):
raise ValueError("time needs ordinary digits")
Year = Digits(Value[0:4], 9999)
Month = Digits(Value[5:7], 12)
Day = Digits(Value[8:10], 31)
Hour = Digits(Value[11:13], 23)
Minute = Digits(Value[14:16], 59)
Second = Digits(Value[17:19], 59)
try:
DateTime(Year, Month, Day)
except ValueError:
raise ValueError("date does not exist") from None
try:
DateTime(Year, Month, Day, Hour, Minute, Second)
except ValueError:
raise ValueError("time does not exist") from None
def ParseEntry(Entry):
try:
if len(Entry.Original) > 1024:
raise ValueError("line exceeds 1024 bytes")
if any(Character != "\t" and not 32 <= ord(Character) <= 126
for Character in Entry.Original):
raise ValueError("unsupported byte in practice format")
Fields = Entry.Original.split("\t")
if len(Fields) != 4:
raise ValueError("expected exactly four tab-separated fields")
CheckTime(Fields[0])
CheckAddress(Fields[1])
CheckAddress(Fields[2])
Entry.Port = Digits(Fields[3], 65535)
if Entry.Port == 0:
raise ValueError("port must be 1 to 65535 in this format")
Entry.EventTime, Entry.Source, Entry.Destination = Fields[:3]
Entry.Accepted = True
except ValueError as Error:
Entry.Problem = str(Error)
return Entry
def ReadLog(FileName):
with Path(FileName).open("rb") as Input:
Data = Input.read(1048577)
if len(Data) > 1048576:
raise ValueError("input exceeds 1 MiB teaching limit")
if not Data:
raise ValueError("empty input")
Lines = Data.split(b"\n")
if Lines[-1] == b"":
Lines.pop()
Lines = [Line[:-1] if Line.endswith(b"\r") else Line for Line in Lines]
if Lines[0] != b"Time\tSource\tDestination\tPort":
raise ValueError("unsupported header")
if len(Lines) - 1 > 1000:
raise ValueError("more than 1000 data lines")
return [ParseEntry(LogEntry(Number, Line.decode("latin-1")))
for Number, Line in enumerate(Lines[1:], start=2)]
The reader handles up to 1,000 data lines in a file of at most 1 MiB, roughly one million bytes, and rejects data lines longer than 1,024 bytes. Its fields use ordinary printable English-character bytes and tabs. Use a saved file that will not change while it is read.
3. Add the comparison
Save this as CompareSummaries.py beside the reader:
import sys as Sys
from LogReader import ReadLog
def CountGroups(FileName):
Groups = {}
Bad = 0
for Entry in ReadLog(FileName):
if Entry.Accepted:
Key = (Entry.Source, Entry.Destination, Entry.Port)
Groups[Key] = Groups.get(Key, 0) + 1
else:
Bad += 1
return Groups, Bad
def Describe(Key):
Source, Destination, Port = Key
return f"{Source} {Destination} {Port}"
def Main():
if len(Sys.argv) != 3:
print("Usage: python3 CompareSummaries.py before.tsv after.tsv", file=Sys.stderr)
return 2
try:
Before, BeforeBad = CountGroups(Sys.argv[1])
After, AfterBad = CountGroups(Sys.argv[2])
except (OSError, ValueError) as Error:
print(f"Input stopped: {Error}", file=Sys.stderr)
return 2
Differences = 0
Same = 0
for Key in sorted(set(Before) | set(After)):
Old = Before.get(Key, 0)
New = After.get(Key, 0)
if Old == 0:
print(f"New: {Describe(Key)} now {New}")
Differences += 1
elif New == 0:
print(f"Disappeared: {Describe(Key)} was {Old}")
Differences += 1
elif Old != New:
print(f"Changed: {Describe(Key)} {Old} -> {New}")
Differences += 1
else:
Same += 1
print(f"Differences: {Differences}; unchanged groups: {Same}.")
print(f"Rejected lines: before {BeforeBad}, after {AfterBad}. A group missing from a file may sit on a rejected line.")
return 1 if Differences or BeforeBad or AfterBad else 0
if __name__ == "__main__":
raise SystemExit(Main())
CountGroups counts accepted entries for each source, destination and port. Main visits groups found in either file, using zero for an absent group, then labels differences as changed, disappeared or new. Equal counts contribute to the unchanged total.
Rejected rows are counted separately. A missing group may have rejected rows. If either file has rejections, inspect those rows with the reader before interpreting the group's disappearance. Repeated accepted rows count again on both sides.
4. Compare the files
Open a command window in the folder and run:
python3 CompareSummaries.py before.tsv after.tsv
Use your installation's Python 3 command if it is not named python3. The output is:
Changed: 192.0.2.10 198.51.100.20 443 2 -> 3
Disappeared: 192.0.2.13 203.0.113.40 25 was 1
New: 192.0.2.14 203.0.113.50 3389 now 1
Differences: 3; unchanged groups: 1.
Rejected lines: before 0, after 0. A group missing from a file may sit on a rejected line.
The port 443 group went from two entries to three. The port 25 group appears only in before.tsv, while port 3389 appears only in after.tsv. The port 53 group has one entry on each side.
The exit code, a small result number another script can check, is 0 for equal groups/counts without rejected rows, 1 for differences or rejections, and 2 when the command or comparison fails. This sample returns 1.
5. Check the direction
Compare the first file with itself:
python3 CompareSummaries.py before.tsv before.tsv
Differences: 0; unchanged groups: 3.
Rejected lines: before 0, after 0. A group missing from a file may sit on a rejected line.
Now reverse the original command's file order. New and disappeared groups swap sides, and changed counts run in the other direction. The labels depend on the starting point, not on why something changed.
Choose comparable collection periods and the same logging setup. A different schedule, missing collection or changed file scope can alter counts without indicating a threat.
The comparison and window programs passed 22 command-line checks, including reversed direction, equal files, rejected rows and load errors. Tested inputs remained unchanged.
Save reports under a new filename: redirecting output over an input can empty it before Python opens it. Keep reports private when they reveal personal or work activity.
References
More free code guides
- Building a Time-Window Log Summary in Python
- Comparing File Contents in Python
- Building a File Fingerprint in Python
- Listing a Folder's Files in Python
- Comparing Folder File Lists in Python
- Checking a Saved File Fingerprint in Python
- Finding Repeated Log Entries in Python
- Finding Time Gaps in a Log with Python
- Counting Log Entries by Minute in Python