[gjs/ewlsh/glogfield-support] glib: Implement override for structured logging hook




commit cea45fa174ae1f4ca0757bd8a650b6daaee6e2e5
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Jul 10 23:33:22 2021 -0700

    glib: Implement override for structured logging hook

 .eslintrc.yml                            |  3 ++
 installed-tests/.eslintrc.yml            |  2 +
 installed-tests/js/jsunit.gresources.xml |  1 +
 installed-tests/js/matchers.js           | 42 +++++++++++++++
 installed-tests/js/meson.build           |  1 +
 installed-tests/js/minijasmine.js        |  3 ++
 installed-tests/js/testGLibLogWriter.js  | 91 ++++++++++++++++++++++++++++++++
 libgjs-private/gjs-util.c                | 91 +++++++++++++++++++++++++++++++-
 libgjs-private/gjs-util.h                | 20 +++++++
 modules/core/overrides/GLib.js           | 41 +++++++++++++-
 10 files changed, 291 insertions(+), 4 deletions(-)
---
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 7ddf0e38..62c34e27 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -7,6 +7,9 @@ env:
 extends: 'eslint:recommended'
 plugins:
   - jsdoc
+settings:
+  jsdoc:
+    mode: typescript
 rules:
   array-bracket-newline:
     - error
diff --git a/installed-tests/.eslintrc.yml b/installed-tests/.eslintrc.yml
index 6c9c0253..780e1a43 100644
--- a/installed-tests/.eslintrc.yml
+++ b/installed-tests/.eslintrc.yml
@@ -3,3 +3,5 @@
 # SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
 rules:
   jsdoc/require-jsdoc: 'off'
+globals:
+  withElements: readonly
diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml
index b635c50c..635a48fe 100644
--- a/installed-tests/js/jsunit.gresources.xml
+++ b/installed-tests/js/jsunit.gresources.xml
@@ -6,6 +6,7 @@
     <file preprocess="xml-stripblanks">complex3.ui</file>
     <file preprocess="xml-stripblanks">complex4.ui</file>
     <file>jasmine.js</file>
+    <file>matchers.js</file>
     <file>minijasmine.js</file>
     <file>modules/alwaysThrows.js</file>
     <file>modules/badOverrides/GIMarshallingTests.js</file>
diff --git a/installed-tests/js/matchers.js b/installed-tests/js/matchers.js
new file mode 100644
index 00000000..4dbde064
--- /dev/null
+++ b/installed-tests/js/matchers.js
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+/* exported registerMatchers */
+
+/* eslint no-redeclare: ["error", { "builtinGlobals": false }] */
+
+/**
+ * A jasmine asymmetric matcher which expects an array-like object
+ * to contain the given element array in the same order with the
+ * same length. Useful for testing typed arrays.
+ *
+ * @template T
+ * @param {T[]} elements an array of elements to compare with
+ * @returns
+ */
+function withElements(elements) {
+    return {
+        /**
+         * @param {ArrayLike<T>} compareTo an array-like object to compare to
+         * @returns {boolean}
+         */
+        asymmetricMatch(compareTo) {
+            return (
+                compareTo.length === elements.length &&
+                elements.every((e, i) => e === compareTo[i])
+            );
+        },
+        /**
+         * @returns {string}
+         */
+        jasmineToString() {
+            return `${JSON.stringify(elements)}`;
+        },
+    };
+}
+
+function registerMatchers() {
+    Object.assign(globalThis, {
+        withElements,
+    });
+}
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index e11f1418..8378bbd7 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -101,6 +101,7 @@ jasmine_tests = [
     'GIMarshalling',
     'Gio',
     'GLib',
+    'GLibLogWriter',
     'GObject',
     'GObjectClass',
     'GObjectInterface',
diff --git a/installed-tests/js/minijasmine.js b/installed-tests/js/minijasmine.js
index a82251a4..56319026 100644
--- a/installed-tests/js/minijasmine.js
+++ b/installed-tests/js/minijasmine.js
@@ -4,6 +4,9 @@
 
 const GLib = imports.gi.GLib;
 
+// Register custom matchers...
+imports.matchers.registerMatchers();
+
 function _removeNewlines(str) {
     let allNewlines = /\n/g;
     return str.replace(allNewlines, '\\n');
diff --git a/installed-tests/js/testGLibLogWriter.js b/installed-tests/js/testGLibLogWriter.js
new file mode 100644
index 00000000..b514a2b4
--- /dev/null
+++ b/installed-tests/js/testGLibLogWriter.js
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+// eslint-disable-next-line
+/// <reference types="jasmine" />
+
+const {GLib} = imports.gi;
+const ByteArray = imports.byteArray;
+
+describe('GLib Structured logging handler', function () {
+    /** @type {jasmine.Spy<(_level: any, _fields: any) => any>} */
+    let writer_func;
+
+    beforeAll(function () {
+        writer_func = jasmine.createSpy(
+            'Log test writer func',
+            function (_level, _fields) {
+                return GLib.LogWriterOutput.HANDLED;
+            }
+        );
+
+        writer_func.and.callThrough();
+
+        GLib.log_set_writer_func(writer_func);
+    });
+
+    beforeEach(function () {
+        writer_func.calls.reset();
+    });
+
+    it('writes a message', function () {
+        GLib.log_structured('Gjs-Console', GLib.LogLevelFlags.LEVEL_MESSAGE, {
+            MESSAGE: 'a message',
+        });
+
+        const bytes = ByteArray.fromString('a message', 'UTF-8');
+
+        expect(writer_func).toHaveBeenCalledWith(
+            GLib.LogLevelFlags.LEVEL_MESSAGE,
+            jasmine.objectContaining({MESSAGE: withElements(bytes)})
+        );
+    });
+
+    it('writes a warning', function () {
+        GLib.log_structured('Gjs-Console', GLib.LogLevelFlags.LEVEL_WARNING, {
+            MESSAGE: 'a warning',
+        });
+
+        const bytes = ByteArray.fromString('a warning', 'UTF-8');
+
+        expect(writer_func).toHaveBeenCalledWith(
+            GLib.LogLevelFlags.LEVEL_WARNING,
+            jasmine.objectContaining({MESSAGE: withElements(bytes)})
+        );
+    });
+
+    it('preserves a custom string field', function () {
+        GLib.log_structured('Gjs-Console', GLib.LogLevelFlags.LEVEL_MESSAGE, {
+            MESSAGE: 'with a custom field',
+            GJS_CUSTOM_FIELD: 'a custom value',
+        });
+
+        const bytes = ByteArray.fromString('with a custom field', 'UTF-8');
+        const custom_bytes = ByteArray.fromString('a custom value', 'UTF-8');
+
+        expect(writer_func).toHaveBeenCalledWith(
+            GLib.LogLevelFlags.LEVEL_MESSAGE,
+            jasmine.objectContaining({
+                MESSAGE: withElements(bytes),
+                GJS_CUSTOM_FIELD: withElements(custom_bytes),
+            })
+        );
+    });
+
+    it('preserves a custom byte array field', function () {
+        GLib.log_structured('Gjs-Console', GLib.LogLevelFlags.LEVEL_MESSAGE, {
+            MESSAGE: 'with a custom field',
+            GJS_CUSTOM_FIELD: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
+        });
+
+        const bytes = ByteArray.fromString('with a custom field', 'UTF-8');
+
+        expect(writer_func).toHaveBeenCalledWith(
+            GLib.LogLevelFlags.LEVEL_MESSAGE,
+            jasmine.objectContaining({
+                MESSAGE: withElements(bytes),
+                GJS_CUSTOM_FIELD: withElements([0, 1, 2, 3, 4, 5, 6, 7]),
+            })
+        );
+    });
+});
diff --git a/libgjs-private/gjs-util.c b/libgjs-private/gjs-util.c
index 15060950..2065f95b 100644
--- a/libgjs-private/gjs-util.c
+++ b/libgjs-private/gjs-util.c
@@ -6,8 +6,10 @@
 
 #include <config.h>
 
-#include <locale.h>    /* for setlocale */
-#include <stddef.h>    /* for size_t */
+#include <locale.h> /* for setlocale */
+#include <stdbool.h>
+#include <stddef.h> /* for size_t */
+#include <string.h>
 
 #include <glib-object.h>
 #include <girepository.h>
@@ -213,3 +215,88 @@ void gjs_list_store_sort(GListStore *store, GjsCompareDataFunc compare_func,
                          void *user_data) {
   g_list_store_sort(store, (GCompareDataFunc)compare_func, user_data);
 }
+
+typedef struct WriterFuncData {
+    GjsGLogWriterFunc func;
+    void* wrapped_user_data;
+    GDestroyNotify wrapped_user_data_free;
+} WriterFuncData;
+
+static void* log_writer_user_data = NULL;
+static GDestroyNotify log_writer_user_data_free = NULL;
+
+GLogWriterOutput gjs_log_writer_func_wrapper(GLogLevelFlags log_level,
+                                             const GLogField* fields,
+                                             size_t n_fields, void* user_data) {
+    GjsGLogWriterFunc func = (GjsGLogWriterFunc)user_data;
+    GVariantDict* dict = g_variant_dict_new(NULL);
+    size_t f;
+    for (f = 0; f < n_fields; f++) {
+        const GLogField* field = &fields[f];
+
+        GVariant* value;
+        if (field->length < 0) {
+            size_t bytes_len = strlen(field->value);
+            GBytes_autoptr bytes = g_bytes_new(field->value, bytes_len);
+
+            value = g_variant_new_maybe(
+                G_VARIANT_TYPE_BYTESTRING,
+                g_variant_new_from_bytes(G_VARIANT_TYPE_BYTESTRING, bytes,
+                                         true));
+        } else if (field->length > 0) {
+            GBytes_autoptr bytes = g_bytes_new(field->value, field->length);
+
+            value = g_variant_new_maybe(
+                G_VARIANT_TYPE_BYTESTRING,
+                g_variant_new_from_bytes(G_VARIANT_TYPE_BYTESTRING, bytes,
+                                         true));
+        } else {
+            value = g_variant_new_maybe(G_VARIANT_TYPE_STRING, NULL);
+        }
+
+        g_variant_dict_insert_value(dict, field->key, value);
+    }
+
+    GVariant* string_fields = g_variant_dict_end(dict);
+    g_variant_ref(string_fields);
+    g_variant_dict_unref(dict);
+
+    GLogWriterOutput output =
+        func(log_level, string_fields, log_writer_user_data);
+
+    g_variant_unref(string_fields);
+    return output;
+}
+
+/**
+ * gjs_log_set_writer_default:
+ *
+ * Sets the structured logging writer function back to the platform default.
+ */
+void gjs_log_set_writer_default() {
+    if (log_writer_user_data_free) {
+        log_writer_user_data_free(log_writer_user_data);
+    }
+
+    g_log_set_writer_func(g_log_writer_default, NULL, NULL);
+    log_writer_user_data_free = NULL;
+    log_writer_user_data = NULL;
+}
+
+/**
+ * gjs_log_set_writer_func:
+ * @func: (scope notified): callback with log data
+ * @user_data: (closure): user data for @func
+ * @user_data_free: (destroy user_data_free): destroy for @user_data
+ *
+ * Sets a given function as the writer function for structured logging,
+ * passing log fields as a variant. If called from JavaScript the application
+ * must call gjs_log_set_writer_default prior to exiting.
+ */
+void gjs_log_set_writer_func(GjsGLogWriterFunc func, void* user_data,
+                             GDestroyNotify user_data_free) {
+    log_writer_user_data = user_data;
+    log_writer_user_data_free = user_data_free;
+
+    g_log_set_writer_func(gjs_log_writer_func_wrapper, func, NULL);
+}
diff --git a/libgjs-private/gjs-util.h b/libgjs-private/gjs-util.h
index 320337c5..8c00ce8a 100644
--- a/libgjs-private/gjs-util.h
+++ b/libgjs-private/gjs-util.h
@@ -48,6 +48,26 @@ GJS_EXPORT
 void gjs_list_store_sort(GListStore *store, GjsCompareDataFunc compare_func,
                          void *user_data);
 
+/**
+ * GjsGLogWriterFunc:
+ * @level: the log level
+ * @fields: a dictionary variant with type a{sms}
+ * @user_data: user data
+ */
+typedef GLogWriterOutput (*GjsGLogWriterFunc)(GLogLevelFlags level,
+                                              const GVariant* fields,
+                                              void* user_data);
+
+GJS_EXPORT
+void gjs_log_set_writer_func(GjsGLogWriterFunc func, gpointer user_data,
+                             GDestroyNotify user_data_free);
+
+GJS_EXPORT
+void gjs_log_set_writer_default();
+
+GJS_EXPORT
+void gjs_log_reset_writer();
+
 /* For imports.gettext */
 typedef enum
 {
diff --git a/modules/core/overrides/GLib.js b/modules/core/overrides/GLib.js
index 5e3800a9..36806e1f 100644
--- a/modules/core/overrides/GLib.js
+++ b/modules/core/overrides/GLib.js
@@ -313,12 +313,49 @@ function _init() {
 
     this.log_structured = function (logDomain, logLevel, stringFields) {
         let fields = {};
-        for (let key in stringFields)
-            fields[key] = new GLib.Variant('s', stringFields[key]);
+        for (let key in stringFields) {
+            const field = stringFields[key];
+
+            if (field instanceof Uint8Array)
+                fields[key] = new GLib.Variant('ay', field);
+            else if (typeof field === 'string')
+                fields[key] = new GLib.Variant('s', field);
+            else if (field instanceof GLib.Variant)
+                fields[key] = field;
+            else
+                throw new TypeError(`Unsupported value ${field}, log_structured supports Uint8Array and 
string types.`);
+        }
 
         GLib.log_variant(logDomain, logLevel, new GLib.Variant('a{sv}', fields));
     };
 
+    // GjsPrivate depends on GLib so we cannot import it
+    // before GLib is fully resolved.
+
+    this.log_set_writer_func_variant = function (...args) {
+        const {log_set_writer_func} = imports.gi.GjsPrivate;
+
+        log_set_writer_func(...args);
+    };
+
+    this.log_set_writer_default = function (...args) {
+        const {log_set_writer_default} = imports.gi.GjsPrivate;
+
+        log_set_writer_default(...args);
+    };
+
+    this.log_set_writer_func = function (writer_func) {
+        const {log_set_writer_func} = imports.gi.GjsPrivate;
+        if (typeof writer_func !== 'function') {
+            log_set_writer_func(writer_func);
+        } else {
+            log_set_writer_func(function (logLevel, stringFields) {
+                const stringFieldsObj = {...stringFields.recursiveUnpack()};
+                return writer_func(logLevel, stringFieldsObj);
+            });
+        }
+    };
+
     this.VariantDict.prototype.lookup = function (key, variantType = null, deep = false) {
         if (typeof variantType === 'string')
             variantType = new GLib.VariantType(variantType);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]