reporting: add new logging type events

Adds new reporting type events "INFO", "WARNING", and "ERROR" to be
used for context logging. These can be invoked with the new `.info`,
`.warning`, and `.error` methods on the context object accordingly.
Useful for things like warning/errors on autoinstall configuartions.
This commit is contained in:
Chris Peterson 2024-03-20 16:03:11 -07:00
parent c25e28ef2c
commit 980411a670
6 changed files with 218 additions and 4 deletions

View File

@ -17,10 +17,17 @@ import copy
import logging import logging
from curtin.reporter import available_handlers, update_configuration from curtin.reporter import available_handlers, update_configuration
from curtin.reporter.events import report_finish_event, report_start_event, status from curtin.reporter.events import (
ReportingEvent,
report_event,
report_finish_event,
report_start_event,
status,
)
from curtin.reporter.handlers import LogHandler as CurtinLogHandler from curtin.reporter.handlers import LogHandler as CurtinLogHandler
from subiquity.server.controller import NonInteractiveController from subiquity.server.controller import NonInteractiveController
from subiquitycore.context import Context
class LogHandler(CurtinLogHandler): class LogHandler(CurtinLogHandler):
@ -76,3 +83,18 @@ class ReportingController(NonInteractiveController):
report_finish_event( report_finish_event(
context.full_name(), description, result, level=context.level context.full_name(), description, result, level=context.level
) )
def report_info_event(self, context: Context, message: str):
"""Report an "info" event."""
event = ReportingEvent("info", context.full_name(), message, level="INFO")
report_event(event)
def report_warning_event(self, context: Context, message: str):
"""Report a "warning" event."""
event = ReportingEvent("warning", context.full_name(), message, level="WARNING")
report_event(event)
def report_error_event(self, context: Context, message: str):
"""Report an "error" event."""
event = ReportingEvent("error", context.full_name(), message, level="ERROR")
report_event(event)

View File

@ -13,11 +13,17 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest.mock import Mock, patch
import jsonschema import jsonschema
from curtin.reporter.events import status as CurtinStatus
from jsonschema.validators import validator_for from jsonschema.validators import validator_for
from subiquity.server.controllers.reporting import ReportingController from subiquity.server.controllers.reporting import ReportingController
from subiquitycore.context import Context
from subiquitycore.context import Status as ContextStatus
from subiquitycore.tests import SubiTestCase from subiquitycore.tests import SubiTestCase
from subiquitycore.tests.mocks import MockedApplication, make_app
class TestReportingController(SubiTestCase): class TestReportingController(SubiTestCase):
@ -29,3 +35,85 @@ class TestReportingController(SubiTestCase):
) )
JsonValidator.check_schema(ReportingController.autoinstall_schema) JsonValidator.check_schema(ReportingController.autoinstall_schema)
@patch("subiquity.server.controllers.reporting.report_event")
class TestReportingCurtinCalls(SubiTestCase):
def setUp(self):
app: MockedApplication = make_app()
self.controller: ReportingController = ReportingController(app)
self.context: Context = app.context
@patch("subiquity.server.controllers.reporting.report_start_event")
def test_start_event(self, report_start_event, report_event):
self.controller.report_start_event(self.context, "description")
# Calls specific start event method
report_start_event.assert_called_with(
self.context.full_name(), "description", level=self.context.level
)
# Not the generic one
report_event.assert_not_called()
@patch("subiquity.server.controllers.reporting.report_finish_event")
def test_finish_event(self, report_finish_event, report_event):
self.controller.report_finish_event(
self.context, "description", ContextStatus.FAIL
)
# Calls specific finish event method
report_finish_event.assert_called_with(
self.context.full_name(),
"description",
CurtinStatus.FAIL,
level=self.context.level,
)
# Not the generic one
report_event.assert_not_called()
# Test default WARN
status = Mock()
status.name = "NEW LEVEL"
self.controller.report_finish_event(self.context, "description", status)
report_finish_event.assert_called_with(
self.context.full_name(),
"description",
CurtinStatus.WARN,
level=self.context.level,
)
@patch("subiquity.server.controllers.reporting.ReportingEvent")
def test_info_event(self, mock_class, report_event):
self.controller.report_info_event(self.context, "description")
mock_class.assert_called_with(
"info",
self.context.full_name(),
"description",
level="INFO",
)
@patch("subiquity.server.controllers.reporting.ReportingEvent")
def test_warning_event(self, mock_class, report_event):
self.controller.report_warning_event(self.context, "description")
mock_class.assert_called_with(
"warning",
self.context.full_name(),
"description",
level="WARNING",
)
@patch("subiquity.server.controllers.reporting.ReportingEvent")
def test_error_event(self, mock_class, report_event):
self.controller.report_error_event(self.context, "description")
mock_class.assert_called_with(
"error",
self.context.full_name(),
"description",
level="ERROR",
)

View File

@ -372,11 +372,14 @@ class SubiquityServer(Application):
# - special sections of the install, which set "is-install-context" # - special sections of the install, which set "is-install-context"
# where we want to report the event anyways # where we want to report the event anyways
# #
# - special event types:
# - warn
# - error
# #
# For non-interactive installs (i.e., full autoinstall) we report # For non-interactive installs (i.e., full autoinstall) we report
# everything. # everything.
force_reporting: bool = install_context force_reporting: bool = install_context or event_type in ["warning", "error"]
# self.interactive=None could be an interactive install, we just # self.interactive=None could be an interactive install, we just
# haven't found out yet # haven't found out yet
@ -388,8 +391,6 @@ class SubiquityServer(Application):
if controller is None or controller.interactive(): if controller is None or controller.interactive():
return return
# Otherwise it came from the server
# Create the message out of the name of the reporter and optionally # Create the message out of the name of the reporter and optionally
# the description # the description
name: str = context.full_name() name: str = context.full_name()
@ -432,6 +433,21 @@ class SubiquityServer(Application):
listener.report_finish_event(context, description, status) listener.report_finish_event(context, description, status)
self._maybe_push_to_journal("finish", context, description) self._maybe_push_to_journal("finish", context, description)
def report_info_event(self, context: Context, message: str) -> None:
for listener in self.event_listeners:
listener.report_info_event(context, message)
self._maybe_push_to_journal("info", context, message)
def report_warning_event(self, context: Context, message: str) -> None:
for listener in self.event_listeners:
listener.report_warning_event(context, message)
self._maybe_push_to_journal("warning", context, message)
def report_error_event(self, context: Context, message: str) -> None:
for listener in self.event_listeners:
listener.report_error_event(context, message)
self._maybe_push_to_journal("error", context, message)
@property @property
def state(self): def state(self):
return self._state return self._state

View File

@ -378,3 +378,81 @@ class TestEventReporting(SubiTestCase):
journal_send_mock.assert_called_once() journal_send_mock.assert_called_once()
else: else:
journal_send_mock.assert_not_called() journal_send_mock.assert_not_called()
@parameterized.expand(
(
# interactive, pushed to journal
(True, False),
(None, False),
(False, True),
)
)
def test_push_info_events(self, interactive, expect_pushed):
"""Test info event publication"""
context: Context = Context(
self.server, "MockContext", "description", None, "INFO"
)
self.server.interactive = interactive
with patch("subiquity.server.server.journal.send") as journal_send_mock:
self.server.report_info_event(context, "message")
if not expect_pushed:
journal_send_mock.assert_not_called()
else:
journal_send_mock.assert_called_once()
# message is the only positional argument
(message,) = journal_send_mock.call_args.args
self.assertIn("message", message)
self.assertNotIn("description", message)
@parameterized.expand(
(
# interactive
(True,),
(None,),
(False,),
)
)
def test_push_warning_events(self, interactive):
"""Test warning event publication"""
context: Context = Context(
self.server, "MockContext", "description", None, "INFO"
)
self.server.interactive = interactive
with patch("subiquity.server.server.journal.send") as journal_send_mock:
self.server.report_warning_event(context, "message")
journal_send_mock.assert_called_once()
# message is the only positional argument
(message,) = journal_send_mock.call_args.args
self.assertIn("message", message)
self.assertNotIn("description", message)
@parameterized.expand(
(
# interactive
(True,),
(None,),
(False,),
)
)
def test_push_error_events(self, interactive):
"""Test error event publication"""
context: Context = Context(
self.server, "MockContext", "description", None, "INFO"
)
self.server.interactive = interactive
with patch("subiquity.server.server.journal.send") as journal_send_mock:
self.server.report_error_event(context, "message")
journal_send_mock.assert_called_once()
# message is the only positional argument
(message,) = journal_send_mock.call_args.args
self.assertIn("message", message)
self.assertNotIn("description", message)

View File

@ -118,6 +118,15 @@ class Context:
c = c.parent c = c.parent
return default return default
def info(self, message: str) -> None:
self.app.report_info_event(self, message)
def warning(self, message: str) -> None:
self.app.report_warning_event(self, message)
def error(self, message: str) -> None:
self.app.report_error_event(self, message)
def with_context(name=None, description="", **context_kw): def with_context(name=None, description="", **context_kw):
def decorate(meth): def decorate(meth):

View File

@ -36,6 +36,7 @@ def make_app(model=None):
app.base_model = model app.base_model = model
else: else:
app.base_model = mock.Mock() app.base_model = mock.Mock()
app.add_event_listener = mock.Mock()
app.controllers = mock.Mock() app.controllers = mock.Mock()
app.context = Context.new(app) app.context = Context.new(app)
app.exit = mock.Mock() app.exit = mock.Mock()