In the previous tutorial, you learned how to scan dependencies for vulnerabilities. But what happens when an attack is already in progress? Without proper logging and monitoring, you will not know until it is too late. In this article, you will learn what to log, what never to log, how to detect attacks, and how to set up meaningful alerts.

The OWASP Top 10 lists “Security Logging and Monitoring Failures” as A09 because most breaches go undetected for months.

Why Security Logging Matters

The average time to detect a data breach is 204 days (IBM Cost of a Data Breach Report 2024). That means attackers have over 6 months to steal data before anyone notices.

Proper logging helps you:

  • Detect attacks in progress — brute force, injection attempts, privilege escalation
  • Investigate incidents — understand what happened, when, and how
  • Meet compliance requirements — GDPR, SOC 2, PCI DSS all require audit logs
  • Improve security over time — learn from past incidents

What to Log (Security Events)

Authentication Events

{
  "timestamp": "2026-06-03T14:30:00Z",
  "event": "login_failed",
  "user_id": "user_123",
  "ip": "203.0.113.42",
  "user_agent": "Mozilla/5.0...",
  "reason": "invalid_password",
  "attempt_count": 3
}

Log these authentication events:

EventWhy
Successful loginTrack normal access patterns
Failed loginDetect brute force attacks
Password changeDetect account takeover
MFA enrollment/removalDetect MFA bypass attempts
Token refreshTrack active sessions
LogoutComplete session lifecycle
Account lockoutDetect brute force threshold

Authorization Events

{
  "timestamp": "2026-06-03T14:35:00Z",
  "event": "authorization_denied",
  "user_id": "user_456",
  "resource": "/api/admin/users",
  "method": "GET",
  "reason": "insufficient_role",
  "required_role": "admin",
  "user_role": "viewer"
}

Log these:

  • Access denied to protected resources
  • Role changes (promotion/demotion)
  • Admin actions (user creation, deletion, config changes)
  • API key creation and revocation

Suspicious Activity

{
  "timestamp": "2026-06-03T14:40:00Z",
  "event": "rate_limit_exceeded",
  "ip": "203.0.113.42",
  "endpoint": "/api/login",
  "requests_per_minute": 150,
  "threshold": 10
}

Log these:

  • Rate limit violations
  • Input validation failures (possible injection attempts)
  • Unusual request patterns (unusual hours, geographic anomalies)
  • File upload of suspicious types
  • Large data exports

What NEVER to Log

This is just as important as what you log. Logging sensitive data creates a new attack target.

Never log:

  • Passwords (plaintext or hashed)
  • Credit card numbers
  • Social Security numbers or national IDs
  • JWT tokens or session tokens
  • API keys or secrets
  • Personal health information
  • Full request bodies with sensitive data

Masking Sensitive Data

Go:

package main

import (
    "fmt"
    "regexp"
    "strings"
)

// Mask email: "user@example.com" -> "u***@example.com"
func maskEmail(email string) string {
    parts := strings.SplitN(email, "@", 2)
    if len(parts) != 2 || len(parts[0]) == 0 {
        return "***"
    }
    return string(parts[0][0]) + "***@" + parts[1]
}

// Mask credit card: "4111111111111111" -> "****1111"
func maskCreditCard(cc string) string {
    if len(cc) < 4 {
        return "****"
    }
    return "****" + cc[len(cc)-4:]
}

// Mask JWT: "eyJhbGciOiJI..." -> "eyJ***"
func maskToken(token string) string {
    if len(token) < 6 {
        return "***"
    }
    return token[:3] + "***"
}

Python:

import re

def mask_email(email):
    """Mask email: user@example.com -> u***@example.com"""
    parts = email.split("@")
    if len(parts) != 2 or not parts[0]:
        return "***"
    return parts[0][0] + "***@" + parts[1]

def mask_credit_card(cc):
    """Mask credit card: 4111111111111111 -> ****1111"""
    if len(cc) < 4:
        return "****"
    return "****" + cc[-4:]

def mask_token(token):
    """Mask token: eyJhbGciOiJI... -> eyJ***"""
    if len(token) < 6:
        return "***"
    return token[:3] + "***"

Structured Logging

Use structured logging (JSON) instead of plain text. Structured logs are easier to search, filter, and analyze.

Go (with slog)

package main

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    // Security event log
    logger.Info("authentication_success",
        slog.String("event", "login"),
        slog.String("user_id", "user_123"),
        slog.String("ip", "203.0.113.42"),
        slog.String("method", "password"),
    )

    // Failed authentication
    logger.Warn("authentication_failed",
        slog.String("event", "login_failed"),
        slog.String("user_id", "user_123"),
        slog.String("ip", "203.0.113.42"),
        slog.String("reason", "invalid_password"),
        slog.Int("attempt", 3),
    )
}

Python (with structlog)

import structlog

logger = structlog.get_logger()

# Security event log
logger.info("authentication_success",
    event="login",
    user_id="user_123",
    ip="203.0.113.42",
    method="password",
)

# Authorization denied
logger.warning("authorization_denied",
    event="access_denied",
    user_id="user_456",
    resource="/api/admin/users",
    reason="insufficient_role",
)

JavaScript (with pino)

const pino = require("pino");
const logger = pino({ level: "info" });

// Security event
logger.info({
  event: "login_failed",
  userId: "user_123",
  ip: "203.0.113.42",
  reason: "invalid_password",
  attemptCount: 3,
}, "Authentication failed");

Log Levels for Security Events

LevelWhen to UseExamples
INFONormal security eventsSuccessful login, password change
WARNSuspicious but not confirmedFailed login, rate limit hit, authorization denied
ERRORSecurity violationsInvalid token, injection attempt detected
CRITICALActive attack or breachMultiple accounts compromised, data exfiltration detected

Detecting Attacks with Log Analysis

Pattern: Brute Force Detection

Python:

import structlog
from collections import defaultdict
from datetime import datetime, timedelta

logger = structlog.get_logger()

# NOTE: In production, use a thread-safe store (e.g., Redis) instead of a
# plain dict. This in-memory dict is not safe for concurrent requests.
failed_logins = defaultdict(list)

def check_brute_force(user_id, ip, timestamp):
    """Detect brute force: 5+ failed logins in 15 minutes"""
    key = f"{user_id}:{ip}"
    cutoff = timestamp - timedelta(minutes=15)

    # Remove old attempts
    failed_logins[key] = [
        t for t in failed_logins[key] if t > cutoff
    ]

    failed_logins[key].append(timestamp)

    if len(failed_logins[key]) >= 5:
        logger.critical("brute_force_detected",
            user_id=user_id,
            ip=ip,
            attempts=len(failed_logins[key]),
            window_minutes=15,
        )
        return True
    return False

Pattern: Anomalous Access

def check_anomalous_access(user_id, ip, country, hour):
    """Flag access from unusual locations or times"""
    usual_countries = get_user_usual_countries(user_id)
    usual_hours = get_user_usual_hours(user_id)

    if country not in usual_countries:
        logger.warning("anomalous_access",
            user_id=user_id,
            ip=ip,
            country=country,
            reason="unusual_country",
        )

    if hour not in usual_hours:
        logger.warning("anomalous_access",
            user_id=user_id,
            hour=hour,
            reason="unusual_hour",
        )

Centralized Logging

Do not store logs on individual servers. Centralize them for analysis.

Architecture

Application -> Log Shipper -> Central Log Store -> Dashboard/Alerts
(stdout)      (Fluent Bit)    (Elasticsearch/     (Grafana/
                               Loki/CloudWatch)    Kibana)
StackComponentsCost
ELKElasticsearch + Logstash + KibanaSelf-hosted (free) or Elastic Cloud
PLGPromtail + Loki + GrafanaSelf-hosted (free) or Grafana Cloud
Cloud-nativeCloudWatch (AWS) / Cloud Logging (GCP)Pay per use
DatadogAgent + Datadog platform~$0.10/GB ingested

Docker Compose with Loki + Grafana

# docker-compose.yml
services:
  app:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"

  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"

  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/log:/var/log
      - ./promtail-config.yml:/etc/promtail/config.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"

Alerting

Logs are useless if nobody reads them. Set up alerts for critical security events.

Alert Rules

AlertConditionSeverity
Brute force attempt10+ failed logins from same IP in 5 minHigh
Account takeoverLogin from new country after password changeCritical
Privilege escalationNon-admin accessing admin endpointsHigh
Rate limit abuse100+ rate limit hits from same IP in 1 minMedium
Injection attemptSQL/XSS patterns in input validation logsHigh
Mass data exportSingle user downloading 10x normal data volumeCritical

Avoid Alert Fatigue

Too many alerts means people ignore them. Keep these rules:

  1. Only alert on actionable events. If nobody needs to do anything, it is a log, not an alert.
  2. Tune thresholds. Start high, lower gradually.
  3. Group related alerts. 50 failed logins from one IP should be one alert, not 50.
  4. Use severity levels. Critical: wake someone up. High: respond today. Medium: respond this week.

Audit Trails

An audit trail is a chronological record of all actions in a system. It answers: who did what, when, and from where.

Requirements for a good audit trail:

  • Immutable — logs cannot be modified or deleted
  • Timestamped — precise timestamps in UTC
  • Complete — every state change is recorded
  • Searchable — easy to find events by user, time, or action

Go example: Audit log middleware

func auditMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := getUserID(r)
        logger.Info("api_access",
            slog.String("user_id", userID),
            slog.String("method", r.Method),
            slog.String("path", r.URL.Path),
            slog.String("ip", r.RemoteAddr),
            slog.String("user_agent", r.UserAgent()),
        )
        next.ServeHTTP(w, r)
    })
}

Log Retention

How long to keep logs depends on regulations and business needs:

RegulationMinimum Retention
GDPRAs long as necessary (minimize)
SOC 21 year
PCI DSS1 year (3 months immediately available)
HIPAA6 years
General best practice90 days hot, 1 year cold storage

Prevention Checklist

DefensePriorityNotes
Log all authentication eventsHighSuccesses and failures
Log authorization denialsHighDetect privilege escalation
Never log passwords, tokens, or PIIHighMask sensitive data
Use structured logging (JSON)HighEasier to search and analyze
Centralize logsHighDo not keep logs on app servers only
Set up alerts for brute forceHigh10+ failed logins in 5 min
Set up alerts for anomalous accessMediumUnusual country or time
Maintain audit trails for admin actionsMediumRequired for compliance
Define log retention policyMediumBased on regulation requirements
Test alerting regularlyLowVerify alerts actually fire

What is Next?

In the next tutorial, you will learn about Container and Docker Security — running containers as non-root, scanning images, managing secrets in Docker, and hardening your deployment.