Skip to content

Commit

Permalink
Merge pull request #932 from SpiNNakerManchester/log_to_db
Browse files Browse the repository at this point in the history
Send log messages to the database
  • Loading branch information
rowleya authored Sep 21, 2022
2 parents 2449438 + e1f99f6 commit f79ec54
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 22 deletions.
7 changes: 2 additions & 5 deletions spinn_front_end_common/interface/config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from spinn_utilities.config_holder import (
config_options, load_config, get_config_bool, get_config_int,
get_config_str, get_config_str_list, set_config)
from spinn_front_end_common.interface.provenance import LogStoreDB
from spinn_front_end_common.data.fec_data_writer import FecDataWriter
from spinn_front_end_common.utilities.exceptions import ConfigurationException

Expand Down Expand Up @@ -65,6 +66,7 @@ def __init__(self, data_writer_cls=None):
self._data_writer = data_writer_cls.setup()
else:
self._data_writer = FecDataWriter.setup()
logger.set_log_store(LogStoreDB())

# set up machine targeted data
self._debug_configs()
Expand Down Expand Up @@ -199,11 +201,6 @@ def _set_up_report_specifics(self):
with open(time_of_run_file_name, "w", encoding="utf-8") as f:
f.writelines(timestamp)

if get_config_bool("Logging", "warnings_at_end_to_file"):
log_report_file = os.path.join(
self._data_writer.get_run_dir_path(), WARNING_LOGS_FILENAME)
logger.set_report_File(log_report_file)

def __write_named_file(self, file_name):
app_file_name = os.path.join(
self._data_writer.get_timestamp_dir_path(), file_name)
Expand Down
3 changes: 2 additions & 1 deletion spinn_front_end_common/interface/provenance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AbstractProvidesLocalProvenanceData)
from .abstract_provides_provenance_data_from_machine import (
AbstractProvidesProvenanceDataFromMachine)
from .log_store_db import LogStoreDB
from .provenance_reader import ProvenanceReader
from .provides_provenance_data_from_machine_impl import (
ProvidesProvenanceDataFromMachineImpl)
Expand All @@ -33,6 +34,6 @@
BUFFER = "BufferExtraction"

__all__ = ["AbstractProvidesLocalProvenanceData",
"AbstractProvidesProvenanceDataFromMachine",
"AbstractProvidesProvenanceDataFromMachine", "LogStoreDB",
"ProvenanceReader", "ProvenanceWriter",
"ProvidesProvenanceDataFromMachineImpl"]
32 changes: 31 additions & 1 deletion spinn_front_end_common/interface/provenance/db.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- Copyright (c) 2018-2019 The University of Manchester
-- Copyright (c) 2018-2022 The University of Manchester
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -218,3 +218,33 @@ CREATE TABLE IF NOT EXISTS boards_provenance(
ip_addres STRING NOT NULL,
ethernet_x INTEGER NOT NULL,
ethernet_y INTEGER NOT NULL);

---------------------------------------------------------------------
-- A table to store log.info
CREATE TABLE IF NOT EXISTS p_log_provenance(
log_id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TIMESTAMP NOT NULL,
level INTEGER NOT NULL,
message STRING NOT NULL);

CREATE TABLE IF NOT EXISTS log_level_names(
level INTEGER PRIMARY KEY NOT NULL,
name STRING NOT NULL);

INSERT OR IGNORE INTO log_level_names
(level, name)
VALUES
(50, "CRITICAL"),
(40, "ERROR"),
(30, "WARNING"),
(20, "INFO"),
(10, "DEBUG");

CREATE VIEW IF NOT EXISTS p_log_view AS
SELECT
timestamp,
name,
message
FROM p_log_provenance left join log_level_names
ON p_log_provenance.level = log_level_names.level
ORDER BY p_log_provenance.log_id;
44 changes: 44 additions & 0 deletions spinn_front_end_common/interface/provenance/log_store_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) 2017-2022 The University of Manchester
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import sqlite3
from spinn_utilities.log_store import LogStore
from spinn_utilities.overrides import overrides
from .provenance_writer import ProvenanceWriter
from .provenance_reader import ProvenanceReader


class LogStoreDB(LogStore):

@overrides(LogStore.store_log)
def store_log(self, level, message, timestamp=None):
try:
with ProvenanceWriter() as db:
db.store_log(level, message, timestamp)
except sqlite3.OperationalError as ex:
if "database is locked" in ex.args:
# Ok ignore this one
# DO NOT log this error here or you will loop forever!
return
# all others are bad
raise

@overrides(LogStore.retreive_log_messages)
def retreive_log_messages(self, min_level=0):
return ProvenanceReader().retreive_log_messages(min_level)

@overrides(LogStore.get_location)
def get_location(self):
return ProvenanceReader.get_last_run_database_path()
19 changes: 17 additions & 2 deletions spinn_front_end_common/interface/provenance/provenance_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class ProvenanceReader(object):

__slots__ = ["_provenance_data_path"]

@staticmethod
def get_last_run_database_path():
@classmethod
def get_last_run_database_path(cls):
""" Get the path of the current provenance database of the last run
.. warning::
Expand Down Expand Up @@ -370,6 +370,21 @@ def messages(self):
"""
return self.run_query(query, [])

def retreive_log_messages(self, min_level=0):
"""
Retrieves all log messages at or above the min_level
:param int min_level:
:rtype: list(tuple(int, str))
"""
query = """
SELECT message
FROM p_log_provenance
WHERE level >= ?
"""
messages = self.run_query(query, [min_level])
return list(map(lambda x: x[0], messages))

@staticmethod
def demo():
""" A demonstration of how to use this class.
Expand Down
52 changes: 44 additions & 8 deletions spinn_front_end_common/interface/provenance/provenance_writer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2017-2019 The University of Manchester
# Copyright (c) 2017-2022 The University of Manchester
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -13,6 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from datetime import datetime
import logging
import os
import re
Expand Down Expand Up @@ -62,7 +63,7 @@ def __init__(self, database_file=None, memory=False):
database_file = os.path.join(
FecDataView.get_provenance_dir_path(), PROVENANCE_DB)
self._database_file = database_file
super().__init__(database_file, ddl_file=_DDL_FILE)
SQLiteDB.__init__(self, database_file, ddl_file=_DDL_FILE)

def insert_version(self, description, the_value):
"""
Expand Down Expand Up @@ -270,12 +271,12 @@ def insert_report(self, message):
VALUES(?)
""", [message])
recorded = cur.lastrowid
cutoff = get_config_int("Reports", "provenance_report_cutoff")
if cutoff is None or recorded < cutoff:
logger.warning(message)
elif recorded == cutoff:
logger.warning(f"Additional interesting provenace items in "
f"{self._database_file}")
cutoff = get_config_int("Reports", "provenance_report_cutoff")
if cutoff is None or recorded < cutoff:
logger.warning(message)
elif recorded == cutoff:
logger.warning(f"Additional interesting provenace items in "
f"{self._database_file}")

def insert_connector(
self, pre_population, post_population, the_type, description,
Expand Down Expand Up @@ -317,3 +318,38 @@ def insert_board_provenance(self, connections):
VALUES (?, ?, ?)
""", ((x, y, ipaddress)
for ((x, y), ipaddress) in connections.items()))

def store_log(self, level, message, timestamp=None):
"""
Stores log messages into the database
:param int level:
:param str message:
"""
if timestamp is None:
timestamp = datetime.now()
with self.transaction() as cur:
cur.execute(
"""
INSERT INTO p_log_provenance(
timestamp, level, message)
VALUES(?, ?, ?)
""",
[timestamp, level, message])

def _test_log_locked(self, text):
"""
THIS IS A TESTING METHOD.
This will lock the database and then try to do a log
"""
with self.transaction() as cur:
# lock the database
cur.execute(
"""
INSERT INTO reports(message)
VALUES(?)
""", [text])
cur.lastrowid
# try logging and storing while locked.
logger.warning(text)
4 changes: 0 additions & 4 deletions spinn_front_end_common/interface/spinnaker.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ critical =
# Note log levels set above does not change the log message reported at the
# end of the run

# Set to true and all log messages warning or above will be written to file
# Set to false these will be shown at the end of the run
warnings_at_end_to_file = False

[Reports]
# If reportsEnabled is false, no text reports are written.
# write_text_specs: If True, produce text version of each Data Spec,
Expand Down
36 changes: 35 additions & 1 deletion unittests/interface/provenance/test_provenance_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
import os
from spinn_utilities.log import FormatAdapter
from datetime import timedelta
from testfixtures.logcapture import LogCapture
import unittest
from spinn_utilities.config_holder import set_config
from spinn_front_end_common.interface.config_setup import unittest_setup
from spinn_front_end_common.interface.provenance import (
ProvenanceWriter, ProvenanceReader)
LogStoreDB, ProvenanceWriter, ProvenanceReader)

logger = FormatAdapter(logging.getLogger(__name__))


class TestProvenanceDatabase(unittest.TestCase):
Expand Down Expand Up @@ -174,3 +179,32 @@ def test_board(self):
data = {(0, 0): '10.11.194.17', (4, 8): '10.11.194.81'}
with ProvenanceWriter() as db:
db.insert_board_provenance(data)

def test_log(self):
db1 = LogStoreDB()
db2 = LogStoreDB()
db1.store_log(30, "this is a warning")
db2.store_log(10, "this is a debug")
db1.store_log(20, "this is an info")
self.assertListEqual(
["this is a warning", "this is a debug", "this is an info"],
db2.retreive_log_messages())
self.assertListEqual(
["this is a warning", "this is an info"],
db1.retreive_log_messages(20))
db2.get_location()

def test_database_locked(self):
ls = LogStoreDB()
logger.set_log_store(ls)
logger.warning("this works")
with ProvenanceWriter() as db:
db._test_log_locked("locked")
logger.warning("not locked")
logger.warning("this wis fine")
# the use of class variables and tests run in parallel dont work.
if "JENKINS_URL" not in os.environ:
self.assertListEqual(
["this works", "not locked", "this wis fine"],
ls.retreive_log_messages(20))
logger.set_log_store(None)

0 comments on commit f79ec54

Please sign in to comment.