08: Logging, CLIs

image.png Մանկական երկաթուղի, լուսանկարի հղումը, Հեղինակ՝ Tigran Kharatyan

Open In Colab (ToDo)

“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.” - Brian Kernighan

📌 Նկարագիր

📚 Ամբողջական նյութը

📺 Տեսագրությունը

📌 Նկարագիր Մեր առաջին սովորած ֆունկցիան՝ print-ը։ Շատ լավ օրեր ենք անցկացրել իրա հետ, բայց հիմա ժամանակն ա անցնել իրա պռոֆեսինոլ տարբերակին՝ logging-ին։ Սովորում ենք ՝ 1. ոնց նշել logging-ի մակարդակը 2. մեր ուզած ձևով ֆոռմատավորել log-երը 3. գրել log-երը ֆայլի մեջ, ինչպես նաև կոնսոլում 4. json ֆորմատով պահել լոգերը 5. exception-ներ log անել

Հետո անցնում ենք Command Line Interface(CLI)-եր սարքելու գրադարաններին։ CLI-ների շնորհիվ ա որ կարանք գրենք ուղղակի pip install panir ու ինքը գնա ավտոմատ քաշի բերի մեր ուզած գրադարանը, ոչ թե մենք բացենք կոդը որտեղ install հրամանը օգտագործվում ա ու կոդի մեջ նշենք որ արգումենտ որպես մեր գրադարանի անունը գնա։ Սովորում ենք՝ 1. Argparse-ը (ներկառուցված գրադարան) 2. Fire (ֆունկցիաներին CLI-ով դիմելու համար 1-2 տողանոց լուծում) 3. Click (դեկորատորներով աշախտող ուժեղ գրադարն) 4. Typer (FastAPI-ենց գրադարանը type hint-երի վրա հիմնված)

📚 Նյութը

Logging

Logging libraries

Docs: 1. Doc 2. Logging Cookbook

Videos: 1. Tech With Tim (15 minute video) 2. Corey Schafer, part 1 (14 minute video) 3. Corey Schafer, part 2 (20 minute video)

Ինչի՞ logging

  1. print-ով մի անգամ աշխատացրեցինք, console ում տպեց ու վերջ, հաջողություն, իսկ logging-ով կարանք հարմար save անենք
  2. պետք չի comment out անել/ջնջել, հետո հետ բերել print-երը, եթե էլ չենք ուզում մի բան տպենք, կարանք սահմամենք որ դեպքում ինչը տպվի
  3. պետք չի ձեռով գրենք երբ ենք տպել ինչ-որ բան կամ որ ֆայլից ա տպվել

First logger + levels

import logging

logging.basicConfig( # Camel case
    level=logging.DEBUG,
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started")      # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred")  # 40
logger.critical("Critical error!") # 50
DEBUG:__main__:This is a debug message
INFO:__main__:Application started
WARNING:__main__:Warning: Potential issue detected
ERROR:__main__:An error occurred
CRITICAL:__main__:Critical error!
import logging

print(logging.DEBUG)
10
import logging

logging.basicConfig( # Camel case
    level=24,
    # format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started") # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred") # 40
logger.critical("Critical error!") # 50
WARNING:__main__:Warning: Potential issue detected
ERROR:__main__:An error occurred
CRITICAL:__main__:Critical error!

Formatting

https://docs.python.org/3/library/logging.html#logrecord-attributes

import logging

logging.basicConfig( # Camel case
    level=24,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logger = logging.getLogger(__name__)

# Different logging levels
logger.debug("This is a debug message") # 10
logger.info("Application started") # 20
logger.warning("Warning: Potential issue detected") # 30
logger.error("An error occurred") # 40
logger.critical("Critical error!") # 50
2025-07-20 10:38:54 - __main__ - WARNING - Warning: Potential issue detected
2025-07-20 10:38:54 - __main__ - ERROR - An error occurred
2025-07-20 10:38:54 - __main__ - CRITICAL - Critical error!

Logging to a file

import logging

logger = logging.getLogger('file_console_logger')
logger.setLevel(logging.DEBUG) # another way to set the level

# Create file handler
file_handler = logging.FileHandler('app.log', mode="a") # a is default
file_handler.setLevel(logging.INFO)

# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Add handlers to logger
logger.addHandler(file_handler)
def divide_numbers(a, b):
    """Division with comprehensive logging"""
    logger.info(f"Starting division: {a} ÷ {b}")
    
    try:
        result = a / b
        logger.info(f"Division successful. Result: {result}")
        return result
    except ZeroDivisionError:
        logger.error("Division by zero error!")
        return None
    except Exception as e:
        logger.critical(f"Unexpected error: {e}")
        return None

# Test the function
result1 = divide_numbers(10, 3)
result2 = divide_numbers(509, 0)

# Show that logs go to both console and file
print(f"\nResults: {result1}, {result2}")
print("Check 'app.log' file for logged messages")
2025-07-20 10:50:40,415 - file_console_logger - INFO - Starting division: 10 ÷ 3
2025-07-20 10:50:40,416 - file_console_logger - INFO - Division successful. Result: 3.3333333333333335
2025-07-20 10:50:40,417 - file_console_logger - INFO - Starting division: 509 ÷ 0
2025-07-20 10:50:40,418 - file_console_logger - ERROR - Division by zero error!

Results: 3.3333333333333335, None
Check 'app.log' file for logged messages

Logging to console as well

import logging

logger = logging.getLogger('file_console_logger')
logger.setLevel(logging.DEBUG) # another way to set the level

# Create file handler
file_handler = logging.FileHandler('app.log', mode="a") # a is default
file_handler.setLevel(logging.INFO)

# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)

# Add handlers to logger
logger.addHandler(file_handler)

Console handler

# add console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter) # Use the same formatter
# Add handlers to logger
logger.addHandler(console_handler)
# Test the function
result1 = divide_numbers(10, 2)
result2 = divide_numbers(10, 0)

# Show that logs go to both console and file
print(f"\nResults: {result1}, {result2}")
print("Check 'app.log' file for logged messages")
2025-07-20 10:50:49,475 - file_console_logger - INFO - Starting division: 10 ÷ 2
2025-07-20 10:50:49,475 - file_console_logger - INFO - Starting division: 10 ÷ 2
2025-07-20 10:50:49,479 - file_console_logger - INFO - Division successful. Result: 5.0
2025-07-20 10:50:49,479 - file_console_logger - INFO - Division successful. Result: 5.0
2025-07-20 10:50:49,482 - file_console_logger - INFO - Starting division: 10 ÷ 0
2025-07-20 10:50:49,482 - file_console_logger - INFO - Starting division: 10 ÷ 0
2025-07-20 10:50:49,485 - file_console_logger - ERROR - Division by zero error!
2025-07-20 10:50:49,485 - file_console_logger - ERROR - Division by zero error!

Results: 5.0, None
Check 'app.log' file for logged messages

Json logging + Rotation file

import json
import logging
from datetime import datetime
from logging.handlers import RotatingFileHandler

# Create advanced logger
advanced_logger = logging.getLogger('advanced_app')
advanced_logger.setLevel(logging.DEBUG)

# Rotating file handler
rotating_handler = RotatingFileHandler(
    'advanced_app.log', 
    maxBytes=1024 / 2,  # 1/2 KB
    backupCount=5
)
rotating_handler.setLevel(logging.INFO)
class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            'timestamp': datetime.fromtimestamp(record.created).isoformat(),
            'level': record.levelname,
            'message': record.getMessage(),
            'module': record.module,
            'function': record.funcName,
            'line': record.lineno
        }
        return json.dumps(log_entry, ensure_ascii=False)

json_formatter = JSONFormatter()

rotating_handler.setFormatter(json_formatter)

advanced_logger.addHandler(rotating_handler)

# Example class with logging
class Calculator:
    def __init__(self):
        self.logger = logging.getLogger(f'{__name__}.Calculator')
        self.logger.info("Calculator instance created")
    
    def add(self, a, b):
        self.logger.debug(f"Adding: {a} + {b}")
        result = a + b
        self.logger.info(f"Addition result: {result}")
        return result
    
    def multiply(self, a, b):
        self.logger.debug(f"Multiplying: {a} × {b}")
        result = a * b
        self.logger.info(f"Multiplication result: {result}")
        return result
    
    def divide(self, a, b):
        self.logger.debug(f"Dividing: {a} ÷ {b}")
        try:
            result = a / b
            self.logger.info(f"Division result: {result}")
            return result
        except ZeroDivisionError:
            self.logger.error("Division by zero attempted")
            return None

# Test the Calculator with logging
calc = Calculator()
calc.add(5, 3)
calc.multiply(4, 7)
calc.divide(10, 2)
calc.divide(10, 0)  # This will generate an error log

advanced_logger.info("Calculator operations completed")
Division by zero attempted
Division by zero attempted
Division by zero attempted

python-json-logger

!uv pip install python-json-logger
Using Python 3.10.18 environment at: C:\Users\hayk_\.conda\envs\lectures
Audited 1 package in 21ms
import sys
import logging
from pythonjsonlogger import jsonlogger   

logger = logging.getLogger(__name__)
stdout_handler = logging.StreamHandler(stream=sys.stdout)

# format with JSON
format_output = jsonlogger.JsonFormatter('%(levelname)s : %(name)s : %(message)s : %(asctime)s')

stdout_handler.setFormatter(format_output)
logger.addHandler(stdout_handler)

logger.error("This is an error message")
logger.critical("This is a critical message")
{"levelname": "ERROR", "name": "__main__", "message": "This is an error message", "asctime": "2025-07-20 11:00:44,850"}
{"levelname": "CRITICAL", "name": "__main__", "message": "This is a critical message", "asctime": "2025-07-20 11:00:44,851"}
import sys
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

stdout_handler = logging.StreamHandler(stream=sys.stdout)
## Create a file handler
fileHandler = logging.FileHandler("app_with_json.log")   # <-

format_output = jsonlogger.JsonFormatter('%(levelname)s : %(name)s : %(message)s : %(asctime)s')

stdout_handler.setFormatter(format_output)

fileHandler.setFormatter(format_output)     # <-

logger.addHandler(stdout_handler)
## the file handle handler
logger.addHandler(fileHandler)             # <-

logger.error("This is an error message")
logger.critical("This is a critical message")
{"levelname": "ERROR", "name": "__main__", "message": "This is an error message", "asctime": "2025-07-20 11:01:06,254"}
{"levelname": "CRITICAL", "name": "__main__", "message": "This is a critical message", "asctime": "2025-07-20 11:01:06,256"}

Exception logging

import logging
try:
    print(509/0)
except Exception as e:
    logging.exception(e)
ERROR:root:division by zero
Traceback (most recent call last):
  File "C:\Users\hayk_\AppData\Local\Temp\ipykernel_6588\1569123368.py", line 3, in <module>
    print(509/0)
ZeroDivisionError: division by zero

Command Line Interfaces (CLIs)

Կոդերը՝ 1. Argparse 2. Fire 3. Click 4. Typer

Հղումներ՝ - Argparse docs - Argparse tutorial - Fire GitHub - Typer GitHub - Click docs

Library One‑line pitch Typical use cases Distinguishing features
Click Decorator‑driven “Command‑Line Interface Creation Kit” Production‑grade tools, multi‑command suites Rich nesting, colors, automatic Bash/Z‑sh completion, context objects; battle‑tested since 2014. (click.palletsprojects.com)
Typer Click‑powered, but driven by type hints Modern Python ≥ 3.8 projects that already lean on typing Generates help & shell completion from function signatures; async‑friendly; FastAPI‑style DX. (PyPI)
Python Fire Zero‑boilerplate – expose any Python object with fire.Fire() Throw‑away scripts, prototyping, data‑science notebooks Introspects classes, dicts, even lambdas; perfect for “I just need a CLI around this function”. (GitHub)

How to choose

  1. Small script, no dependencies? Stay on argparse.
  2. You want nicer syntax, colors, nested commands? Click is the mainstream choice.
  3. Already writing async / FastAPI‑style code with type hints? Typer feels natural.
  4. You have a pre‑written module and need a CLI now. Drop in Python Fire.

🎲 00 (ToDo)

Flag Counter