Add logging API (#417)

This commit is contained in:
antoniofilipovic 2022-08-22 13:47:52 +02:00 committed by GitHub
parent 705631a35d
commit d73d153978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 281 additions and 62 deletions

View File

@ -1292,6 +1292,12 @@ struct mgp_proc;
/// Describes a Memgraph magic function.
struct mgp_func;
/// All available log levels that can be used in mgp_log function
MGP_ENUM_CLASS mgp_log_level{
MGP_LOG_LEVEL_TRACE, MGP_LOG_LEVEL_DEBUG, MGP_LOG_LEVEL_INFO,
MGP_LOG_LEVEL_WARN, MGP_LOG_LEVEL_ERROR, MGP_LOG_LEVEL_CRITICAL,
};
/// Entry-point for a query module read procedure, invoked through openCypher.
///
/// Passed in arguments will not live longer than the callback's execution.
@ -1386,6 +1392,9 @@ enum mgp_error mgp_proc_add_result(struct mgp_proc *proc, const char *name, stru
/// Return mgp_error::MGP_ERROR_INVALID_ARGUMENT if `name` is not a valid result name.
/// RETURN mgp_error::MGP_ERROR_LOGIC_ERROR if a result field with the same name was already added.
enum mgp_error mgp_proc_add_deprecated_result(struct mgp_proc *proc, const char *name, struct mgp_type *type);
/// Log a message on a certain level.
enum mgp_error mgp_log(enum mgp_log_level log_level, const char *output);
///@}
/// @name Execution

View File

@ -2004,4 +2004,82 @@ def _wrap_exceptions():
setattr(module, name, wrap_function(obj))
class Logger:
"""Represents a Logger through which it is possible
to send logs via API to the graph database.
The best way to use this Logger is to have one per query module."""
__slots__ = ("_logger",)
def __init__(self):
self._logger = _mgp._LOGGER
def info(self, out: str) -> None:
"""
Log message on INFO level..
Args:
out: String message to be logged.
Examples:
```logger.info("Hello from query module.")```
"""
self._logger.info(out)
def warning(self, out: str) -> None:
"""
Log message on WARNING level..
Args:
out: String message to be logged.
Examples:
```logger.warning("Hello from query module.")```
"""
self._logger.warning(out)
def critical(self, out: str) -> None:
"""
Log message on CRITICAL level..
Args:
out: String message to be logged.
Examples:
```logger.critical("Hello from query module.")```
"""
self._logger.critical(out)
def error(self, out: str) -> None:
"""
Log message on ERROR level..
Args:
out: String message to be logged.
Examples:
```logger.error("Hello from query module.")```
"""
self._logger.error(out)
def trace(self, out: str) -> None:
"""
Log message on TRACE level..
Args:
out: String message to be logged.
Examples:
```logger.trace("Hello from query module.")```
"""
self._logger.trace(out)
def debug(self, out: str) -> None:
"""
Log message on DEBUG level..
Args:
out: String message to be logged.
Examples:
```logger.debug("Hello from query module.")```
"""
self._logger.debug(out)
_wrap_exceptions()

View File

@ -2796,3 +2796,29 @@ mgp_error mgp_module_add_function(mgp_module *module, const char *name, mgp_func
},
result);
}
mgp_error mgp_log(const mgp_log_level log_level, const char *output) {
return WrapExceptions([=] {
switch (log_level) {
case mgp_log_level::MGP_LOG_LEVEL_TRACE:
spdlog::trace(output);
return;
case mgp_log_level::MGP_LOG_LEVEL_DEBUG:
spdlog::debug(output);
return;
case mgp_log_level::MGP_LOG_LEVEL_INFO:
spdlog::info(output);
return;
case mgp_log_level::MGP_LOG_LEVEL_WARN:
spdlog::warn(output);
return;
case mgp_log_level::MGP_LOG_LEVEL_ERROR:
spdlog::error(output);
return;
case mgp_log_level::MGP_LOG_LEVEL_CRITICAL:
spdlog::critical(output);
return;
}
throw std::invalid_argument{fmt::format("Invalid log level: {}", log_level)};
});
}

View File

@ -2052,6 +2052,81 @@ PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex) {
return py_path;
}
// clang-format off
struct PyLogger {
PyObject_HEAD
};
// clang-format on
PyObject *PyLoggerLog(PyLogger *self, PyObject *args, const mgp_log_level level) {
MG_ASSERT(self);
const char *out = nullptr;
if (!PyArg_ParseTuple(args, "s", &out)) {
return nullptr;
}
if (RaiseExceptionFromErrorCode(mgp_log(level, out))) {
return nullptr;
}
Py_RETURN_NONE;
}
PyObject *PyLoggerLogInfo(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_INFO);
}
PyObject *PyLoggerLogWarning(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_WARN);
}
PyObject *PyLoggerLogError(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_ERROR);
}
PyObject *PyLoggerLogCritical(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_CRITICAL);
}
PyObject *PyLoggerLogTrace(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_TRACE);
}
PyObject *PyLoggerLogDebug(PyLogger *self, PyObject *args) {
return PyLoggerLog(self, args, mgp_log_level::MGP_LOG_LEVEL_DEBUG);
}
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static PyMethodDef PyLoggerMethods[] = {
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
{"info", reinterpret_cast<PyCFunction>(PyLoggerLogInfo), METH_VARARGS,
"Logs a message with level INFO on this logger."},
{"warning", reinterpret_cast<PyCFunction>(PyLoggerLogWarning), METH_VARARGS,
"Logs a message with level WARNNING on this logger."},
{"error", reinterpret_cast<PyCFunction>(PyLoggerLogError), METH_VARARGS,
"Logs a message with level ERROR on this logger."},
{"critical", reinterpret_cast<PyCFunction>(PyLoggerLogCritical), METH_VARARGS,
"Logs a message with level CRITICAL on this logger."},
{"trace", reinterpret_cast<PyCFunction>(PyLoggerLogTrace), METH_VARARGS,
"Logs a message with level TRACE on this logger."},
{"debug", reinterpret_cast<PyCFunction>(PyLoggerLogDebug), METH_VARARGS,
"Logs a message with level DEBUG on this logger."},
{nullptr},
};
// clang-format off
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static PyTypeObject PyLoggerType = {
PyVarObject_HEAD_INIT(nullptr, 0)
.tp_name = "_mgp.Logger",
.tp_basicsize = sizeof(PyLogger),
// NOLINTNEXTLINE(hicpp-signed-bitwise)
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "Logging API.",
.tp_methods = PyLoggerMethods,
};
// clang-format on
struct PyMgpError {
const char *name;
PyObject *&exception;
@ -2103,6 +2178,7 @@ PyObject *PyInitMgpModule() {
if (!register_type(&PyCypherTypeType, "Type")) return nullptr;
if (!register_type(&PyMessagesType, "Messages")) return nullptr;
if (!register_type(&PyMessageType, "Message")) return nullptr;
if (!register_type(&PyLoggerType, "Logger")) return nullptr;
std::array py_mgp_errors{
PyMgpError{"_mgp.UnknownError", gMgpUnknownError, PyExc_RuntimeError, nullptr},
@ -2169,8 +2245,14 @@ auto WithMgpModule(mgp_module *module_def, const TFun &fun) {
"import a new module. Is some other thread also importing Python "
"modules?");
auto *py_query_module = MakePyQueryModule(module_def);
MG_ASSERT(py_query_module);
MG_ASSERT(py_mgp.SetAttr("_MODULE", py_query_module));
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
auto *py_logger = reinterpret_cast<PyObject *>(PyObject_New(PyLogger, &PyLoggerType));
MG_ASSERT(py_mgp.SetAttr("_LOGGER", py_logger));
auto ret = fun();
auto maybe_exc = py::FetchError();
MG_ASSERT(py_mgp.SetAttr("_MODULE", Py_None));

View File

@ -13,11 +13,25 @@ import mgp
@mgp.read_proc
def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
object: mgp.Any) -> mgp.Record(mutable=bool):
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
return mgp.Record(mutable=object.underlying_graph_is_mutable())
@mgp.read_proc
def graph_is_mutable(ctx: mgp.ProcCtx) -> mgp.Record(mutable=bool):
return mgp.Record(mutable=ctx.graph.is_mutable())
@mgp.read_proc
def log_message(ctx: mgp.ProcCtx, message: str) -> mgp.Record(success=bool):
logger = mgp.Logger()
try:
logger.info(message)
logger.critical(message)
logger.trace(message)
logger.debug(message)
logger.warning(message)
logger.error(message)
except RuntimeError:
return mgp.Record(success=False)
return mgp.Record(success=True)

View File

@ -13,8 +13,7 @@ import typing
import mgclient
import sys
import pytest
from common import (execute_and_fetch_all,
has_one_result_row, has_n_result_row)
from common import execute_and_fetch_all, has_one_result_row, has_n_result_row
def test_is_write(connection):
@ -22,15 +21,19 @@ def test_is_write(connection):
result_order = "name, signature, is_write"
cursor = connection.cursor()
for proc in execute_and_fetch_all(
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
"is_write WHERE name STARTS WITH 'write' "
f"RETURN {result_order}"):
cursor,
"CALL mg.procedures() YIELD * WITH name, signature, "
"is_write WHERE name STARTS WITH 'write' "
f"RETURN {result_order}",
):
assert proc[is_write] is True
for proc in execute_and_fetch_all(
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
"is_write WHERE NOT name STARTS WITH 'write' "
f"RETURN {result_order}"):
cursor,
"CALL mg.procedures() YIELD * WITH name, signature, "
"is_write WHERE NOT name STARTS WITH 'write' "
f"RETURN {result_order}",
):
assert proc[is_write] is False
assert cursor.description[0].name == "name"
@ -41,8 +44,7 @@ def test_is_write(connection):
def test_single_vertex(connection):
cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
result = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")
result = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")
vertex = result[0][0]
assert isinstance(vertex, mgclient.Node)
assert has_one_result_row(cursor, "MATCH (n) RETURN n")
@ -50,14 +52,10 @@ def test_single_vertex(connection):
assert vertex.properties == {}
def add_label(label: str):
execute_and_fetch_all(
cursor, f"MATCH (n) CALL write.add_label(n, '{label}') "
"YIELD * RETURN *")
execute_and_fetch_all(cursor, f"MATCH (n) CALL write.add_label(n, '{label}') " "YIELD * RETURN *")
def remove_label(label: str):
execute_and_fetch_all(
cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') "
"YIELD * RETURN *")
execute_and_fetch_all(cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') " "YIELD * RETURN *")
def get_vertex() -> mgclient.Node:
return execute_and_fetch_all(cursor, "MATCH (n) RETURN n")[0][0]
@ -65,8 +63,10 @@ def test_single_vertex(connection):
def set_property(property_name: str, property: typing.Any):
nonlocal cursor
execute_and_fetch_all(
cursor, f"MATCH (n) CALL write.set_property(n, '{property_name}', "
"$property) YIELD * RETURN *", {"property": property})
cursor,
f"MATCH (n) CALL write.set_property(n, '{property_name}', " "$property) YIELD * RETURN *",
{"property": property},
)
label_1 = "LABEL1"
label_2 = "LABEL2"
@ -89,24 +89,23 @@ def test_single_vertex(connection):
set_property(property_name, None)
assert get_vertex().properties == {}
execute_and_fetch_all(
cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
execute_and_fetch_all(cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
def test_single_edge(connection):
cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
edge_type = "EDGE"
edge = execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, '{edge_type}') "
"YIELD e RETURN e")[0][0]
cursor,
f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, '{edge_type}') "
"YIELD e RETURN e",
)[0][0]
assert edge.type == edge_type
assert edge.properties == {}
@ -120,9 +119,10 @@ def test_single_edge(connection):
def set_property(property_name: str, property: typing.Any):
nonlocal cursor
execute_and_fetch_all(
cursor, "MATCH ()-[e]->() "
f"CALL write.set_property(e, '{property_name}', "
"$property) YIELD * RETURN *", {"property": property})
cursor,
"MATCH ()-[e]->() " f"CALL write.set_property(e, '{property_name}', " "$property) YIELD * RETURN *",
{"property": property},
)
set_property(property_name, property_value_1)
assert get_edge().properties == {property_name: property_value_1}
@ -130,64 +130,74 @@ def test_single_edge(connection):
assert get_edge().properties == {property_name: property_value_2}
set_property(property_name, None)
assert get_edge().properties == {}
execute_and_fetch_all(
cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
execute_and_fetch_all(cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
def test_detach_delete_vertex(connection):
cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
cursor,
f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, 'EDGE') "
"YIELD e RETURN e")
"YIELD e RETURN e",
)
assert has_one_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m")
execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
"CALL write.detach_delete_vertex(n) YIELD * RETURN 1")
cursor, f"MATCH (n) WHERE id(n) = {v1_id} " "CALL write.detach_delete_vertex(n) YIELD * RETURN 1"
)
assert has_n_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m", 0)
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
assert has_one_result_row(
cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
assert has_one_result_row(cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
def test_graph_mutability(connection):
cursor = connection.cursor()
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
v1_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
execute_and_fetch_all(
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
cursor,
f"MATCH (n) WHERE id(n) = {v1_id} "
f"MATCH (m) WHERE id(m) = {v2_id} "
f"CALL write.create_edge(n, m, 'EDGE') "
"YIELD e RETURN e")
"YIELD e RETURN e",
)
def test_mutability(is_write: bool):
module = "write" if is_write else "read"
assert execute_and_fetch_all(
cursor, f"CALL {module}.graph_is_mutable() "
"YIELD mutable RETURN mutable")[0][0] is is_write
assert execute_and_fetch_all(
cursor, "MATCH (n) "
f"CALL {module}.underlying_graph_is_mutable(n) "
"YIELD mutable RETURN mutable")[0][0] is is_write
assert execute_and_fetch_all(
cursor, "MATCH (n)-[e]->(m) "
f"CALL {module}.underlying_graph_is_mutable(e) "
"YIELD mutable RETURN mutable")[0][0] is is_write
assert (
execute_and_fetch_all(cursor, f"CALL {module}.graph_is_mutable() " "YIELD mutable RETURN mutable")[0][0]
is is_write
)
assert (
execute_and_fetch_all(
cursor, "MATCH (n) " f"CALL {module}.underlying_graph_is_mutable(n) " "YIELD mutable RETURN mutable"
)[0][0]
is is_write
)
assert (
execute_and_fetch_all(
cursor,
"MATCH (n)-[e]->(m) " f"CALL {module}.underlying_graph_is_mutable(e) " "YIELD mutable RETURN mutable",
)[0][0]
is is_write
)
test_mutability(True)
test_mutability(False)
def test_log_message(connection):
cursor = connection.cursor()
success = execute_and_fetch_all(cursor, f"CALL read.log_message('message') YIELD success RETURN success")[0][0]
assert (success) is True
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-rA"]))