Rewrite logging function to log to seq and console; Add function to change SQL driver depends on os; Add requirements.exe

This commit is contained in:
Igor Barcik 2024-11-21 14:47:49 +01:00
parent fc3aeb9a14
commit 7447dd8e43
Signed by: biggy
GPG Key ID: EA4CE0D1E2A6DC98
4 changed files with 117 additions and 26 deletions

View File

@ -1,38 +1,29 @@
import logging
import os import os
import platform
import socket import socket
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Tuple from typing import List, Tuple
import pyodbc import pyodbc
import seqlog
import snap7 import snap7
from dotenv import load_dotenv from dotenv import load_dotenv
from snap7.util.getters import get_lreal, get_ulint from snap7.util.getters import get_lreal, get_ulint
load_dotenv() load_dotenv()
# Determine the correct driver based on OS
SQL_DRIVER = "ODBC Driver 18 for SQL Server" if platform.system() == "Linux" else "SQL Server"
# Configuration # Configuration
CONN_STR = ( CONN_STR = (
f"Driver={{SQL Server}};" f"Driver={{{SQL_DRIVER}}};"
f"Server={os.getenv('DB_SERVER')};" f"Server={os.getenv('DB_SERVER')};"
f"Database={os.getenv('DB_NAME')};" f"Database={os.getenv('DB_NAME')};"
f"UID={os.getenv('DB_USER')};" f"UID={os.getenv('DB_USER')};"
f"PWD={os.getenv('DB_PASSWORD')};" f"PWD={os.getenv('DB_PASSWORD')};"
"TrustServerCertificate=yes;"
) )
# Logger setup
root_logger = logging.getLogger()
# Logger setup
seq_logger = seqlog.log_to_seq(
server_url=os.getenv("SEQ_URL"),
api_key=os.getenv("SEQ_API_KEY"),
level=logging.INFO,
support_extra_properties=True,
)
@dataclass @dataclass
class PlcConfig: class PlcConfig:
""" """
@ -53,10 +44,13 @@ class PlcConfig:
id: int id: int
ip: str ip: str
db_number: int db_number: int
is_enabled: bool
air_offset: int air_offset: int
energy_offset: int energy_offset: int
runstatus_offset: int location: str
is_enabled: bool last_energy_read: float
last_air_read: float
runstop_status_offset: int
@dataclass @dataclass
@ -79,7 +73,7 @@ class DatabaseManager:
with pyodbc.connect(self.conn_str) as conn: with pyodbc.connect(self.conn_str) as conn:
with conn.cursor() as cursor: with conn.cursor() as cursor:
cursor.execute(""" cursor.execute("""
SELECT Id, Ip, DbNumber, AirDbOffset, EnergyDbOffset, RunStatusDbOffset,IsEnable SELECT Id, Ip, DbNumber, IsEnable, AirDbOffset, EnergyDbOffset, Location, LastEnergyRead, LastAirRead, RunStopStatusDbOffset
FROM sch.Plc FROM sch.Plc
WHERE IsEnable = 1 WHERE IsEnable = 1
""") """)
@ -145,9 +139,9 @@ class PlcManager:
try: try:
plc.connect(config.ip, 0, 1) plc.connect(config.ip, 0, 1)
db_data = plc.db_read( db_data = plc.db_read(
config.db_number, config.air_offset, config.energy_offset + 10 config.db_number, 0, config.runstop_status_offset
) # Read up to energy offset + 8 bytes ) # Read up to energy offset + 8 bytes
air_value = get_ulint(db_data, config.air_offset) air_value = get_lreal(db_data, config.air_offset)
# TODO if (air_value < prev_air_val): update in plc # TODO if (air_value < prev_air_val): update in plc
energy_value = get_lreal(db_data, config.energy_offset) energy_value = get_lreal(db_data, config.energy_offset)
stauts_value = getattr(db_data, config.runstatus_offset) stauts_value = getattr(db_data, config.runstatus_offset)

37
logger_setup.py Normal file
View File

@ -0,0 +1,37 @@
import logging
import os
import json
import seqlog
class CustomFormatter(logging.Formatter):
def format(self, record):
if hasattr(record, "extra"):
extra_formatted = json.dumps(record.extra, indent=2)
return f"{self.formatTime(record)} [{record.levelname}] {record.getMessage()}\nExtra Data: {extra_formatted}"
return f"{self.formatTime(record)} [{record.levelname}] {record.getMessage()}"
def setup_logger():
logger = logging.getLogger("IndustrialEnergyTracker")
logger.setLevel(logging.DEBUG)
# Console handler with custom formatting
console_handler = logging.StreamHandler()
console_handler.setFormatter(CustomFormatter())
logger.addHandler(console_handler)
# SEQ logging if configured with optimized batch settings
seq_url = os.getenv("SEQ_URL")
seq_api_key = os.getenv("SEQ_API_KEY")
if seq_url and seq_api_key:
seqlog.log_to_seq(
server_url=seq_url,
api_key=seq_api_key,
level=logging.DEBUG,
batch_size=50, # Increased batch size
auto_flush_timeout=10, # Increased flush timeout to 10 seconds
)
return logger

60
main.py
View File

@ -1,21 +1,44 @@
import os
import time import time
from datetime import datetime from datetime import datetime
import pprint
from energy_monitor import CONN_STR, DatabaseManager, PlcManager, root_logger from energy_monitor import CONN_STR, DatabaseManager, PlcManager
from logger_setup import setup_logger
# Globals # Globals
DEVICE_DELAY = 1 # Delay between device data fetches DEVICE_DELAY = 1 # Delay between device data fetches
POOL_RATE = 2 # 2s polling rate reading of NextRead value from database POOL_RATE = 2 # 2s polling rate reading of NextRead value from database
FALLBACK_INTERVAL = 3600 # 1h interval in case db read issue FALLBACK_INTERVAL = 20 # 1h = 3600s interval in case db read issue
# Logger setup
logger = setup_logger()
def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
plc_configs = db_manager.get_plc_configs() plc_configs = db_manager.get_plc_configs()
# If debug print out plc_configs to logs
logger.debug(
"Retrieved PLC configurations",
extra={
"plc_configs": [
{
"id": plc.id,
"ip": plc.ip,
"location": plc.location,
"is_enabled": plc.is_enabled,
"db_number": plc.db_number,
}
for plc in plc_configs
],
"timestamp": datetime.now().isoformat(),
},
)
for plc_config in plc_configs: for plc_config in plc_configs:
try: try:
if not plc_manager.check_connection(plc_config.ip): if not plc_manager.check_connection(plc_config.ip):
root_logger.error( logger.error(
"PLC connection failed", "PLC connection failed",
extra={ extra={
"PlcId": plc_config.id, "PlcId": plc_config.id,
@ -28,7 +51,7 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
air_value, energy_value = plc_manager.read_plc_data(plc_config) air_value, energy_value = plc_manager.read_plc_data(plc_config)
db_manager.save_energy_data(plc_config.id, energy_value, air_value, True) db_manager.save_energy_data(plc_config.id, energy_value, air_value, True)
root_logger.info( logger.info(
"✅ Data successfully read and saved", "✅ Data successfully read and saved",
extra={ extra={
"PlcId": plc_config.id, "PlcId": plc_config.id,
@ -42,7 +65,7 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
time.sleep(DEVICE_DELAY) # n-second delay between devices time.sleep(DEVICE_DELAY) # n-second delay between devices
except Exception as e: except Exception as e:
root_logger.error( logger.error(
"Error processing PLC", "Error processing PLC",
extra={ extra={
"PlcId": plc_config.id, "PlcId": plc_config.id,
@ -54,6 +77,23 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
def main(): def main():
pprint.pp(
{
"SQL_Config": {
"Server": os.getenv("DB_SERVER"),
"Database": os.getenv("DB_NAME"),
"User": os.getenv("DB_USER"),
# Masking password for security
"Password": "****",
},
"SEQ_Config": {
"URL": os.getenv("SEQ_URL"),
# Masking API key for security
"API_Key": "****",
},
"timestamp": datetime.now().isoformat(),
}
)
db_manager = DatabaseManager(CONN_STR) db_manager = DatabaseManager(CONN_STR)
plc_manager = PlcManager() plc_manager = PlcManager()
@ -68,7 +108,13 @@ def main():
time.sleep(POOL_RATE) time.sleep(POOL_RATE)
except Exception as e: except Exception as e:
root_logger.error( pprint.pp(
{
"error": str(e),
"timestamp": datetime.now().isoformat(),
}
)
logger.error(
"Main loop error! Falling back to 1h interval.", "Main loop error! Falling back to 1h interval.",
extra={"error": str(e), "timestamp": datetime.now().isoformat()}, extra={"error": str(e), "timestamp": datetime.now().isoformat()},
) )

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
# requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
pyodbc==5.2.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-snap7==2.0.2
PyYAML==6.0.2
requests==2.32.3
seqlog==0.3.31
six==1.16.0
urllib3==2.2.3