Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions helm/robusta/templates/runner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ spec:
value: {{ .Values.runner.sendAdditionalTelemetry | quote }}
- name: LOG_LEVEL
value: {{ .Values.runner.log_level }}
- name: ENABLE_JSON_LOGS_FORMAT
value: {{ .Values.global.enableJsonLogsFormat | default false | quote }}
- name: INSTALLATION_NAMESPACE
valueFrom:
fieldRef:
Expand Down
4 changes: 4 additions & 0 deletions helm/robusta/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ clusterZone: ""

global:
clusterDomain: "cluster.local"
# When true, Robusta components emit logs as JSON (one object per line) instead
# of colored text, which is easier to index with log scrapers like Filebeat.
# This is shared with subcharts (e.g. Holmes) automatically.
enableJsonLogsFormat: false
# Optional image pull secrets applied to the runner, kubewatch, and the runtime pods
# created by the runner (e.g. KRR, Popeye, via the runner ServiceAccount).
# A component's own imagePullSecrets (e.g. runner.imagePullSecrets), when set, is used
Expand Down
11 changes: 10 additions & 1 deletion playbooks/robusta_playbooks/krr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
action,
JobEvent,
)
from robusta.core.model.env_vars import INSTALLATION_NAMESPACE, RELEASE_NAME, CLUSTER_DOMAIN, SET_KRR_SECURITY_CONTEXT, load_bool
from robusta.core.model.env_vars import ENABLE_JSON_LOGS_FORMAT, INSTALLATION_NAMESPACE, RELEASE_NAME, CLUSTER_DOMAIN, SET_KRR_SECURITY_CONTEXT, load_bool
from robusta.core.reporting.consts import ScanState
from robusta.integrations.openshift import IS_OPENSHIFT
from robusta.integrations.prometheus.utils import generate_prometheus_config
Expand Down Expand Up @@ -516,6 +516,15 @@ def krr_scan(event: ExecutionBaseEvent, params: KRRParams):
seccompProfile=SeccompProfile(type="RuntimeDefault")
)

# Mirror the runner's JSON log setting onto the KRR scan job, but ONLY when
# KRR_PUSH_SCAN is enabled. In that mode results are POSTed back via
# --publish_scan_url and the job's logs are not parsed. When KRR_PUSH_SCAN is
# false the result is extracted by json.loads()-ing the job's captured logs
# (see below), and pod.get_logs() returns the combined stdout+stderr stream,
# so JSON-formatted log lines would corrupt that parsing.
if ENABLE_JSON_LOGS_FORMAT and KRR_PUSH_SCAN:
env_var.append(EnvVar(name="ENABLE_JSON_LOGS_FORMAT", value="true"))

spec = PodSpec(
serviceAccountName=params.serviceAccountName,
containers=[
Expand Down
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ include_trailing_comma = true
python = ">=3.10, <3.12"
setuptools = "^80.9.0"
colorlog = "^5.0.1"
python-json-logger = "^3.0.0"
pydantic = "^1.8.1"
kubernetes = "^26.1.0"
pymsteams = "^0.1.16"
Expand Down
4 changes: 4 additions & 0 deletions src/robusta/core/model/env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def load_bool(env_var, default: bool):
DISABLE_DISCOVERY = load_bool("DISABLE_DISCOVERY", False)
DISABLE_RESOURCE_WATCH_PERSISTENCE = load_bool("DISABLE_RESOURCE_WATCH_PERSISTENCE", False)

# When true, logs are emitted as JSON (one object per line) instead of the default
# colored text format. Useful for log scrapers like Filebeat. Defaults to false.
ENABLE_JSON_LOGS_FORMAT = load_bool("ENABLE_JSON_LOGS_FORMAT", False)

PROMETHEUS_ERROR_LOG_PERIOD_SEC = int(os.environ.get("DISCOVERY_MAX_BATCHES", 14400))

RRM_PERIOD_SEC = int(os.environ.get("RRM_PERIOD_SEC", 90))
Expand Down
29 changes: 26 additions & 3 deletions src/robusta/runner/log_init.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import logging
import os
import os.path
import sys

import colorlog
from pythonjsonlogger.json import JsonFormatter

from robusta.core.model.env_vars import ENABLE_JSON_LOGS_FORMAT


def init_logging():
logging_level = os.environ.get("LOG_LEVEL", "INFO")
logging_format = "%(log_color)s%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s"
logging_datefmt = "%Y-%m-%d %H:%M:%S"

print("setting up colored logging")
colorlog.basicConfig(format=logging_format, level=logging_level, datefmt=logging_datefmt)
if ENABLE_JSON_LOGS_FORMAT:
# JSON logs (one object per line) are easier for log scrapers like
# Filebeat to index, search, and filter. Rename levelname -> severity
# to match the convention used across Robusta services (relay, holmes).
# Avoid printing anything to stdout here so the JSON stream is not
# corrupted by a plain-text line.
handler = logging.StreamHandler(sys.stdout)
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(handlers=[handler], level=logging_level, force=True)
else:
logging_format = "%(log_color)s%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s"
# force=True so a prior configuration (e.g. a JSON handler from an
# earlier init_logging() call) is replaced, mirroring the JSON branch.
colorlog.basicConfig(
format=logging_format, level=logging_level, datefmt=logging_datefmt, force=True
)

logging.getLogger().setLevel(logging_level)
for logger_name in ["werkzeug", "telethon"]:
Expand Down
73 changes: 73 additions & 0 deletions tests/test_log_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import importlib
import json
import logging
from io import StringIO

import pytest


@pytest.fixture(autouse=True)
def restore_root_logger():
"""Snapshot and restore the root logger so handlers don't leak between tests."""
root = logging.getLogger()
saved_handlers = root.handlers[:]
saved_level = root.level
try:
yield
finally:
root.handlers = saved_handlers
root.setLevel(saved_level)


def _reload_log_init(monkeypatch, json_enabled: bool):
"""Reload env_vars + log_init so ENABLE_JSON_LOGS_FORMAT is re-read from env."""
monkeypatch.setenv("ENABLE_JSON_LOGS_FORMAT", "true" if json_enabled else "false")

import robusta.core.model.env_vars as env_vars

importlib.reload(env_vars)

import robusta.runner.log_init as log_init

importlib.reload(log_init)
return log_init


def _capture_root_output() -> StringIO:
"""Attach a StringIO stream handler to the root logger and return the buffer."""
buffer = StringIO()
handler = logging.StreamHandler(buffer)
handler.setFormatter(logging.getLogger().handlers[0].formatter)
logging.getLogger().addHandler(handler)
return buffer
Comment thread
moshemorad marked this conversation as resolved.


def test_json_logging_emits_valid_json(monkeypatch):
log_init = _reload_log_init(monkeypatch, json_enabled=True)
log_init.init_logging()

buffer = _capture_root_output()
logging.getLogger().info("hello json")

line = buffer.getvalue().strip().splitlines()[-1]
payload = json.loads(line)
assert payload["message"] == "hello json"
# levelname is renamed to severity to match the other Robusta services.
assert payload["severity"] == "INFO"


def test_plain_logging_is_not_json(monkeypatch):
log_init = _reload_log_init(monkeypatch, json_enabled=False)
log_init.init_logging()

buffer = _capture_root_output()
logging.getLogger().info("hello text")

line = buffer.getvalue().strip().splitlines()[-1]
assert "hello text" in line
try:
json.loads(line)
is_json = True
except json.JSONDecodeError:
is_json = False
assert not is_json
Loading