diff --git a/enforcer/enforcer_main.py b/enforcer/enforcer_main.py index 4f9a3d8e..ed9aed12 100644 --- a/enforcer/enforcer_main.py +++ b/enforcer/enforcer_main.py @@ -23,6 +23,7 @@ from fastapi import FastAPI, HTTPException, Response from pydantic import BaseModel from prometheus_client import generate_latest, CONTENT_TYPE_LATEST +from pythonjsonlogger.json import JsonFormatter from enforcer.dal.supabase_dal import SupabaseDal from enforcer.patch_manager import patch_container_resources @@ -35,7 +36,16 @@ # Configure logging logger = logging.getLogger() logHandler = logging.StreamHandler(sys.stdout) -formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +# When ENABLE_JSON_LOGS_FORMAT is set, emit JSON logs (one object per line) so +# scrapers like Filebeat can index them; otherwise keep the plain text format. +if os.environ.get("ENABLE_JSON_LOGS_FORMAT", "false").strip().lower() in ("true", "1", "yes"): + formatter: logging.Formatter = JsonFormatter( + fmt="%(asctime)s %(levelname)s %(name)s %(filename)s %(lineno)d %(funcName)s %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + rename_fields={"levelname": "severity"}, + ) +else: + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") logHandler.setFormatter(formatter) logger.addHandler(logHandler) logger.setLevel(os.environ.get("LOG_LEVEL", "INFO")) diff --git a/enforcer/requirements.txt b/enforcer/requirements.txt index 2415c8a2..c72e8f49 100644 --- a/enforcer/requirements.txt +++ b/enforcer/requirements.txt @@ -5,6 +5,7 @@ supabase==2.5 PyYAML==6.0.1 cachetools==5.3.3 prometheus-client==0.20.0 +python-json-logger==3.3.0 kubernetes==26.1.0 pyasn1>=0.6.2 urllib3==2.7.0 diff --git a/helm/krr-enforcer/templates/enforcer.yaml b/helm/krr-enforcer/templates/enforcer.yaml index df59018e..28dde8e8 100644 --- a/helm/krr-enforcer/templates/enforcer.yaml +++ b/helm/krr-enforcer/templates/enforcer.yaml @@ -118,6 +118,8 @@ spec: value: "/etc/webhook/certs/tls.crt" - name: LOG_LEVEL value: {{ .Values.logLevel | quote }} + - name: ENABLE_JSON_LOGS_FORMAT + value: {{ .Values.global.enableJsonLogsFormat | default .Values.enableJsonLogsFormat | default false | quote }} {{- if .Values.certificate }} - name: CERTIFICATE value: {{ .Values.certificate | quote }} diff --git a/helm/krr-enforcer/values.yaml b/helm/krr-enforcer/values.yaml index 030d2ec0..ee969e01 100644 --- a/helm/krr-enforcer/values.yaml +++ b/helm/krr-enforcer/values.yaml @@ -1,5 +1,12 @@ certificate: "" # base64 encoded logLevel: INFO +# When true, logs are emitted as JSON (one object per line) instead of plain +# text. Useful for log scrapers like Filebeat. Defaults to false. +enableJsonLogsFormat: false + +# Values shared with/from a parent umbrella chart. Declared so templates can +# safely reference .Values.global.* when the chart is installed standalone. +global: {} # Certificate job configuration certJob: diff --git a/poetry.lock b/poetry.lock index 90b1d4e6..ce07ec8a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1380,6 +1380,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-json-logger" +version = "3.3.0" +description = "JSON Log Formatter for the Python Logging Package" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7"}, + {file = "python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] + [[package]] name = "pytokens" version = "0.4.1" @@ -1941,4 +1955,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<=3.12.9" -content-hash = "718445c7b4b883ce590028f1f05596a1245136cf03ebb9d0df4ca29ce42fe293" +content-hash = "4ec8f2f17a60af871ffdeb0af94625b01b8173929102c96a0a8513a60f5a9367" diff --git a/pyproject.toml b/pyproject.toml index 2b34dac1..8c015889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ urllib3 = "^2.7.0" setuptools = "^80.9.0" zipp = "^3.19.1" tenacity = "^9.0.0" +python-json-logger = "^3.0.0" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index 14862bf4..6e416147 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ pydantic==1.10.15 ; python_version >= "3.10" and python_full_version < "3.13" pygments==2.20.0 ; python_version >= "3.10" and python_full_version < "3.13" pyparsing==3.1.2 ; python_version >= "3.10" and python_full_version < "3.13" python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_full_version < "3.13" +python-json-logger==3.3.0 ; python_version >= "3.10" and python_full_version < "3.13" pytz==2024.1 ; python_version >= "3.10" and python_full_version < "3.13" pyyaml==6.0.1 ; python_version >= "3.10" and python_full_version < "3.13" regex==2023.12.25 ; python_version >= "3.10" and python_full_version < "3.13" diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py index a771c3c9..e423c6c7 100644 --- a/robusta_krr/core/models/config.py +++ b/robusta_krr/core/models/config.py @@ -1,12 +1,14 @@ from __future__ import annotations import logging +import os import sys from typing import Any, Literal, Optional, Union import pydantic as pd from kubernetes import config from kubernetes.config.config_exception import ConfigException +from pythonjsonlogger.json import JsonFormatter from rich.console import Console from rich.logging import RichHandler @@ -203,12 +205,26 @@ def set_config(config: Config) -> None: global _config _config = config - logging.basicConfig( - level="NOTSET", - format="%(message)s", - datefmt="[%X]", - handlers=[RichHandler(console=config.logging_console)], - ) + # ENABLE_JSON_LOGS_FORMAT is env-driven (never a CLI flag) so the + # interactive CLI keeps its Rich output, while in-cluster scan jobs can + # emit JSON logs (one object per line) for scrapers like Filebeat. + if os.environ.get("ENABLE_JSON_LOGS_FORMAT", "false").strip().lower() in ("true", "1", "yes"): + handler = logging.StreamHandler(config.logging_console.file) + handler.setFormatter( + JsonFormatter( + fmt="%(asctime)s %(levelname)s %(name)s %(filename)s %(lineno)d %(funcName)s %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + rename_fields={"levelname": "severity"}, + ) + ) + logging.basicConfig(level="NOTSET", handlers=[handler], force=True) + else: + logging.basicConfig( + level="NOTSET", + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler(console=config.logging_console)], + ) logging.getLogger("").setLevel(logging.CRITICAL) logger.setLevel(logging.DEBUG if config.verbose else logging.CRITICAL if config.quiet else logging.INFO)