NetAuto Academy

Advanced Python Automation for Network Engineers

Logging Instead of Print: Structured, Secret-Safe Output

Advanced11 min read

What you'll learn

  • Why print() stops being sufficient once a tool has multiple modules
  • The four logging levels this module actually uses and when each applies
  • How to configure logging once and get context automatically in every message
  • Why a token or password must never appear in a log line
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

print() doesn't scale past one file

Every capstone's print() calls worked because there was one file, run once, watched by one person. Once code is split across modules (Lesson 2) and possibly run concurrently (Lesson 7), print() has no levels, no timestamps, and no way to tell which module a message came from.

network_automation/logging_config.py
import logging

def configure_logging(level=logging.INFO):
  logging.basicConfig(
      level=level,
      format="%(asctime)s %(levelname)s %(name)s: %(message)s",
  )

def get_logger(name):
  return logging.getLogger(name)

configure_logging() is called once, at startup. get_logger(__name__) gives each module its own named logger -- %(name)s in the format string is what makes the source of each message visible.

Quick checkConcept check
What does print() lack that becomes a real problem once code is split across multiple modules?

Real output, from real code

using it in automation.py
from network_automation.logging_config import get_logger

logger = get_logger(__name__)

def check_device(connection, device):
  hostname = device.hostname
  try:
      actual_vlans = get_state_with_retry(connection, hostname)
  except DeviceUnreachableError as e:
      logger.warning(f"{hostname}: unreachable ({e})")
      return AutomationResult(hostname=hostname, status=Status.UNREACHABLE, detail=str(e))
  # ...
  logger.info(f"{hostname}: already compliant")
  return AutomationResult(hostname=hostname, status=Status.ALREADY_COMPLIANT, detail="no change needed")

logger.info() for the normal case, logger.warning() for a device-level failure that isn't a bug — the same expected-failure-vs-real-bug distinction from Lesson 3, now reflected in which level gets used.

output — real run against all 5 devices
2026-08-26 13:00:20,139 INFO network_automation.automation: SW1: already compliant
2026-08-26 13:00:20,447 INFO network_automation.automation: SW2: missing VLANs [10]
2026-08-26 13:00:21,573 INFO network_automation.automation: SW3: missing VLANs [30]
2026-08-26 13:00:28,612 WARNING network_automation.automation: SW4: unreachable (SW4 timed out)
2026-08-26 13:00:28,919 WARNING network_automation.automation: SW5: authentication failed (SW5 rejected the credentials)

Real, verified output. Every line carries a timestamp, a level, and network_automation.automation as the source — %(name)s reflecting get_logger(__name__) exactly.

Quick checkIdentify the correct code
Which level is correct for logging that SW4 is unreachable — a real, expected operational failure, not a bug?
Quick checkPredict output
Given the format string "%(asctime)s %(levelname)s %(name)s: %(message)s", what does %(name)s resolve to for a log call made from automation.py via get_logger(__name__)?

What must never appear in a log line

A token in a log file is a leaked credential

It's tempting to log a request for debugging, headers and all. Don't. A log file is often kept longer, read by more people, and stored less carefully than the code itself — logging an Authorization header is functionally the same mistake as hardcoding the credential in source, the rule every module since Netmiko Fundamentals has held. This module's own logger.info()/logger.warning() calls above log hostnames and statuses only — never a token, never a password.

Quick checkMyth check
Is it acceptable to log the full request, including the Authorization header, when debugging a connection issue?

Key takeaways

  • print() lacks levels, timestamps, and a source module — all real problems once code is split across files (Lesson 2) or run concurrently (Lesson 7).
  • configure_logging() runs once at startup; get_logger(__name__) gives each module its own named logger.
  • logger.info() for the normal/expected case, logger.warning() for an operational failure — verified directly against all 5 devices' real output.
  • A token or password must never appear in a log line — the same no-hardcoded-credentials rule since Netmiko Fundamentals, applied to logging output specifically.