Merge lp:~dobey/unity-api/add-simple-logger into lp:unity-api

Proposed by dobey
Status: Needs review
Proposed branch: lp:~dobey/unity-api/add-simple-logger
Merge into: lp:unity-api
Prerequisite: lp:~dobey/unity-api/add-snap-util
Diff against target: 329 lines (+265/-0)
8 files modified
include/unity/util/Logger.h (+60/-0)
src/unity/util/CMakeLists.txt (+1/-0)
src/unity/util/Logger.cpp (+77/-0)
test/gtest/unity/util/CMakeLists.txt (+1/-0)
test/gtest/unity/util/Logger/CMakeLists.txt (+19/-0)
test/gtest/unity/util/Logger/Logger_test.cpp (+105/-0)
test/headers/CMakeLists.txt (+1/-0)
test/headers/includechecker.py (+1/-0)
To merge this branch: bzr merge lp:~dobey/unity-api/add-simple-logger
Reviewer Review Type Date Requested Status
Michi Henning (community) Needs Fixing
Unity8 CI Bot continuous-integration Needs Fixing
Review via email: mp+321480@code.launchpad.net

Commit message

Add the simple logging wrapper around glib logging API.

To post a comment you must log in.
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
279. By dobey

Not actually using gmock here.

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :

PASSED: Continuous integration, rev:279
https://unity8-jenkins.ubuntu.com/job/lp-unity-api-ci/168/
Executed test runs:
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build/4787
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-0-fetch/4815
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=amd64,release=xenial+overlay/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=amd64,release=xenial+overlay/4638/artifact/output/*zip*/output.zip
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=amd64,release=zesty/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=amd64,release=zesty/4638/artifact/output/*zip*/output.zip
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=armhf,release=xenial+overlay/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=armhf,release=xenial+overlay/4638/artifact/output/*zip*/output.zip
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=armhf,release=zesty/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=armhf,release=zesty/4638/artifact/output/*zip*/output.zip
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=i386,release=xenial+overlay/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=i386,release=xenial+overlay/4638/artifact/output/*zip*/output.zip
    SUCCESS: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=i386,release=zesty/4638
        deb: https://unity8-jenkins.ubuntu.com/job/build-2-binpkg/arch=i386,release=zesty/4638/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://unity8-jenkins.ubuntu.com/job/lp-unity-api-ci/168/rebuild

review: Approve (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :

This doesn't strike me as the correct approach. All the overloads of operator<<() shouldn't be necessary. Why not use an approach more like the Logger class in scopes-api?

It uses a LogStream class that derives from std::ostringstream and stores a reference to the actual ostream to write to. That makes it unnecessary to overload operator<<(), and all the usual mechanisms for allowing user-defined types to be inserted can still be used (such as adding an overloaded operator<<() for a custom type to namespace std).

LogStream can only be move constructed, so there are no life cycle issues with the reference to the real ostream.

I would do away with the whole maybe_space() thing altogether. I find it a *huge* pain in the butt that qDebug() and its ilk insist on adding spaces all the time. More often than not, when cobbling messages together, I end up having to use nospace() (and noquote()) all the time to stop it from messing with my output.

I'd *much* prefer having something that behaves just like a normal ostream. With that, I can also use manipulators to change formatting, and use all the other good things that come with ostreams.

Other niceties, such as adding a time stamp, are possible that way too.

The macros for debug(), warn(), etc. truly suck. Please let's not do this. They tear right through all the namespaces and can cause truly strange errors due to name clashes with something else called "debug" elsewhere (which is quite likely). We can just return a LogStream by value from a function instead (and the implementation of the function can tie it to the correct output stream).

It's not clear to me that this should be a header-only implementation. I'm with you on the desire for low overhead. But this is a heavy-weight implementation already because each and every message that is written hits the heap (and ostreams are not light-weight either).

In the interest of ABI stability, I strongly suspect that it would be better to pimpl the entire implementation. In terms of performance, it will not make an iota of difference, but it'll shield us from problems down the track in terms of ABI stability and applications not picking up new features until they are recompiled (as is the case for header-only).

review: Needs Fixing
280. By dobey

Move some code into C++ and greatly simplify the implementation.

Revision history for this message
dobey (dobey) wrote :

Well, I made the Logger class an ostringstream based class, got rid of all the operator overrides, moved the ctor/dtor bits into a compiled file, and made it much simpler.

However, the macros have to be the way they are, because of the dependency on the G_LOG_DOMAIN value being defined at compile time. I wasn't able to get around this yet, but am open to suggestions. The goal with this was to have something as simple to use as qDebug() but which uses the g_log functionality, so we automatically get the G_DEBUG_MESSAGES env var handling at runtime, as a means of being consistent across the platform.

I unfortunately am also not able to get the output buffer overriding working right in the tests. It seems to work fine for the debug() case, but fails for all the other cases, regardless of how I order them.

I looked at what's in unity-scopes-api and it seems a bit complex for what I was wanting to do here.

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michi Henning (michihenning) wrote :
Download full text (4.8 KiB)

I don't see the need for macros at all. The G_LOG_DOMAIN seems to be a red herring. The underlying g_log() function accepts the domain as a parameter. As far as I can see, G_LOG_DOMAIN is used only by the g_error() and similar macros so you don't have to provide the domain at every point where a log message is produced.

We can do the same thing by having the logger instance hang onto the domain and supply it when a message is written with g_log(). No need for a macro that way.

We can't have the implementation without using a pimpl. It needs to be split into a public and internal API, otherwise we can't change the implementation without breaking ABI. The only data member of the Logger (and the LogWriter, see below) should be a unique_ptr to the implementation in the internal namespace.

Here is how I would approach it:

class Logger
{
public:
    Logger(std::string const& domain);

    Logger(Logger const&);
    Logger(Logger&&);
    Logger& operator=(Logger const&);
    Logger& operator=(Logger&&);
    virtual ~Logger();

    // ...
};

That's a well-behaved class with the big five taken care of. It does *not* derive from ostringstream. It simply is a class that remembers its domain in a data member (of the internal Impl class).

Now add a method to read the domain (for completeness) and a method that writes a log message:

class Logger
{
public:
    // ...

    std::string domain() const;
    LogWriter operator()(LogLevel l);
};

With that, I can write:

Logger log("mydomain");
log(LogLevel::debug) << "hello";

To make things a little more convenient, we can add these:

class Logger
{
public:
    // ...

    LogWriter operator()(); // Synonym for debug()
    LogWriter debug();
    LogWriter info();
    LogWriter message();
    LogWriter warning();
    LogWriter critical();
    LogWriter error();
};

Now I can write:

Logger log("mydomain");
log() << "debug";
log.warning() << "warning";

The LogWriter class takes care of writing the actual log message (note the derivation from ostringstream). I've omitted the pimpl here for brevity. The only data member should be a unique_ptr<internal::LogWriterImpl>; the two data members here should be part of LogWriterImpl:

class LogWriter : public std::ostringstream
{
public:
    LogWriter(LogWriter&&);
    LogWriter& operator=(LogWriter&&) = delete;
    ~LogWriter();

private:
    LogWriter(std::string const& domain);

    // There should be a unique_ptr<internal::LogWriteImpl> here instead.
    // This is only for exposition:
    std::string domain_;
    std::ostringstream str_;

    friend class Logger;
};

The LogWriter can be constructed with a domain only by the Logger. The API client can only move-construct a LogWriter, which enforces that the only way to use it is via the methods on Logger that return a LogWriter.

The LogWriter move constructor looks like this:

LogWriter::LogWriter(LogWriter&& other)
    : ostringstream(std::move(other))
    , domain_(other.domain_)
{
}

The destructor writes the message. Here, I'm just writing to clog, but the method could write using g_log() instead just as easily, passing in the domain:

LogWriter::~LogWriter()
{
    string msg = str();
    if (!msg.empty(...

Read more...

review: Needs Fixing

Unmerged revisions

280. By dobey

Move some code into C++ and greatly simplify the implementation.

279. By dobey

Not actually using gmock here.

278. By dobey

Add the simple logging wrapper around glib logging API.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'include/unity/util/Logger.h'
2--- include/unity/util/Logger.h 1970-01-01 00:00:00 +0000
3+++ include/unity/util/Logger.h 2017-04-03 21:10:57 +0000
4@@ -0,0 +1,60 @@
5+/*
6+ * Copyright 2015-2017 Canonical Ltd.
7+ *
8+ * This library is free software; you can redistribute it and/or
9+ * modify it under the terms of version 3 of the GNU Lesser General Public
10+ * License as published by the Free Software Foundation.
11+ *
12+ * This program is distributed in the hope that it will be useful,
13+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+ * General Public License for more details.
16+ *
17+ * You should have received a copy of the GNU Lesser General Public
18+ * License along with this library; if not, write to the
19+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20+ * Boston, MA 02110-1301, USA.
21+ */
22+
23+#pragma once
24+
25+#include <sstream>
26+
27+namespace unity
28+{
29+
30+namespace util
31+{
32+
33+ class Logger: public std::ostringstream
34+ {
35+ public:
36+ enum class Level {
37+ DEBUG = 0,
38+ INFO,
39+ MESSAGE,
40+ WARNING,
41+ CRITICAL,
42+ ERROR,
43+ };
44+
45+ Logger(Logger::Level level, const std::string& domain);
46+ Logger(Logger&& logger);
47+
48+ virtual ~Logger();
49+
50+ private:
51+ Level _level;
52+ std::string _domain;
53+ };
54+
55+// These are the APIs to be used for logging.
56+#define debug() Logger(Logger::Level::DEBUG, G_LOG_DOMAIN)
57+#define info() Logger(Logger::Level::INFO, G_LOG_DOMAIN)
58+#define warn() Logger(Logger::Level::WARNING, G_LOG_DOMAIN)
59+#define critical() Logger(Logger::Level::CRITICAL, G_LOG_DOMAIN)
60+#define error() Logger(Logger::Level::ERROR, G_LOG_DOMAIN)
61+
62+} // namespace util
63+
64+} // namespace unity
65
66=== modified file 'src/unity/util/CMakeLists.txt'
67--- src/unity/util/CMakeLists.txt 2017-04-03 21:10:57 +0000
68+++ src/unity/util/CMakeLists.txt 2017-04-03 21:10:57 +0000
69@@ -4,6 +4,7 @@
70 ${CMAKE_CURRENT_SOURCE_DIR}/Daemon.cpp
71 ${CMAKE_CURRENT_SOURCE_DIR}/FileIO.cpp
72 ${CMAKE_CURRENT_SOURCE_DIR}/IniParser.cpp
73+ ${CMAKE_CURRENT_SOURCE_DIR}/Logger.cpp
74 ${CMAKE_CURRENT_SOURCE_DIR}/SnapPath.cpp
75 )
76
77
78=== added file 'src/unity/util/Logger.cpp'
79--- src/unity/util/Logger.cpp 1970-01-01 00:00:00 +0000
80+++ src/unity/util/Logger.cpp 2017-04-03 21:10:57 +0000
81@@ -0,0 +1,77 @@
82+/*
83+ * Copyright 2017 Canonical Ltd.
84+ *
85+ * This library is free software; you can redistribute it and/or
86+ * modify it under the terms of version 3 of the GNU Lesser General Public
87+ * License as published by the Free Software Foundation.
88+ *
89+ * This program is distributed in the hope that it will be useful,
90+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
91+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
92+ * General Public License for more details.
93+ *
94+ * You should have received a copy of the GNU Lesser General Public
95+ * License along with this library; if not, write to the
96+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
97+ * Boston, MA 02110-1301, USA.
98+ */
99+
100+#define G_LOG_DOMAIN nullptr
101+
102+#include "unity/util/Logger.h"
103+
104+#include <glib.h>
105+
106+namespace unity
107+{
108+
109+namespace util
110+{
111+
112+Logger::Logger(Logger::Level level, const std::string& domain):
113+ std::ostringstream(""),
114+ _level(level),
115+ _domain(domain)
116+{
117+}
118+
119+Logger::Logger(Logger&& logger):
120+ std::ostringstream(std::move(logger)),
121+ _level(logger._level)
122+{
123+}
124+
125+Logger::~Logger()
126+{
127+ GLogLevelFlags glib_level;
128+ auto message = str();
129+
130+ if (!message.empty()) {
131+ switch (_level) {
132+ case Logger::Level::DEBUG:
133+ glib_level = G_LOG_LEVEL_DEBUG;
134+ break;
135+ case Logger::Level::INFO:
136+ glib_level = G_LOG_LEVEL_INFO;
137+ break;
138+ case Logger::Level::MESSAGE:
139+ glib_level = G_LOG_LEVEL_MESSAGE;
140+ break;
141+ case Logger::Level::WARNING:
142+ glib_level = G_LOG_LEVEL_WARNING;
143+ break;
144+ case Logger::Level::CRITICAL:
145+ glib_level = G_LOG_LEVEL_CRITICAL;
146+ break;
147+ case Logger::Level::ERROR:
148+ glib_level = G_LOG_LEVEL_ERROR;
149+ break;
150+ }
151+ g_log(_domain.empty() ? nullptr : _domain.c_str(),
152+ glib_level, "%s", message.c_str());
153+ }
154+}
155+
156+} // namespace util
157+
158+} // namespace unity
159
160=== modified file 'test/gtest/unity/util/CMakeLists.txt'
161--- test/gtest/unity/util/CMakeLists.txt 2017-04-03 21:10:57 +0000
162+++ test/gtest/unity/util/CMakeLists.txt 2017-04-03 21:10:57 +0000
163@@ -4,6 +4,7 @@
164 add_subdirectory(GlibMemory)
165 add_subdirectory(GObjectMemory)
166 add_subdirectory(IniParser)
167+add_subdirectory(Logger)
168 add_subdirectory(ResourcePtr)
169 add_subdirectory(SnapPath)
170 add_subdirectory(internal)
171
172=== added directory 'test/gtest/unity/util/Logger'
173=== added file 'test/gtest/unity/util/Logger/CMakeLists.txt'
174--- test/gtest/unity/util/Logger/CMakeLists.txt 1970-01-01 00:00:00 +0000
175+++ test/gtest/unity/util/Logger/CMakeLists.txt 2017-04-03 21:10:57 +0000
176@@ -0,0 +1,19 @@
177+find_package(Threads)
178+find_package (PkgConfig REQUIRED)
179+pkg_check_modules(GLIB REQUIRED glib-2.0)
180+add_definitions(
181+ ${GLIB_CLFAGS}
182+ ${GLIB_CFLAGS_OTHER}
183+ -DG_LOG_DOMAIN="Logger_test"
184+)
185+include_directories(${GLIB_INCLUDE_DIRS})
186+
187+add_executable(Logger_test Logger_test.cpp)
188+target_link_libraries(Logger_test
189+ ${LIBS}
190+ ${TESTLIBS}
191+ ${GLIB_LDFLAGS}
192+ ${CMAKE_THREAD_LIBS_INIT}
193+)
194+
195+add_test(Logger Logger_test)
196
197=== added file 'test/gtest/unity/util/Logger/Logger_test.cpp'
198--- test/gtest/unity/util/Logger/Logger_test.cpp 1970-01-01 00:00:00 +0000
199+++ test/gtest/unity/util/Logger/Logger_test.cpp 2017-04-03 21:10:57 +0000
200@@ -0,0 +1,105 @@
201+/*
202+ * Copyright 2015-2017 Canonical Ltd.
203+ *
204+ * This library is free software; you can redistribute it and/or
205+ * modify it under the terms of version 3 of the GNU Lesser General Public
206+ * License as published by the Free Software Foundation.
207+ *
208+ * This program is distributed in the hope that it will be useful,
209+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
210+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
211+ * General Public License for more details.
212+ *
213+ * You should have received a copy of the GNU Lesser General Public
214+ * License along with this library; if not, write to the
215+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
216+ * Boston, MA 02110-1301, USA.
217+ */
218+
219+#include <unity/util/Logger.h>
220+
221+#include <glib.h>
222+#include <gtest/gtest.h>
223+#include <unistd.h>
224+
225+using namespace unity::util;
226+
227+namespace {
228+
229+class LoggerTest : public ::testing::Test
230+{
231+public:
232+ LoggerTest()
233+ {
234+ _handler_id = g_log_set_handler(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
235+ _log_handler, this);
236+ std::cerr << "Set up handler: " << _handler_id << std::endl;
237+ }
238+
239+ virtual ~LoggerTest()
240+ {
241+ if (_handler_id != 0) {
242+ g_log_remove_handler(G_LOG_DOMAIN, _handler_id);
243+ }
244+ }
245+
246+ std::string str()
247+ {
248+ return _stream.str();
249+ }
250+
251+private:
252+ static void _log_handler(const gchar*, GLogLevelFlags,
253+ const gchar* message, gpointer gthis)
254+ {
255+ static_cast<LoggerTest*>(gthis)->log_message(message);
256+ }
257+
258+ void log_message(const char* message)
259+ {
260+ std::cerr << "Message: " << message << std::endl;
261+ _stream << message;
262+ usleep(200);
263+ }
264+
265+ std::ostringstream _stream;
266+ guint _handler_id;
267+};
268+
269+TEST_F(LoggerTest, testDebug)
270+{
271+ auto expected = "Some data about things happening.";
272+ debug() << expected;
273+ ASSERT_STREQ(expected, str().c_str());
274+}
275+
276+TEST_F(LoggerTest, testInfo)
277+{
278+ auto expected = "Some info.";
279+ info() << expected;
280+ ASSERT_STREQ(expected, str().c_str());
281+}
282+
283+TEST_F(LoggerTest, testWarning)
284+{
285+ auto expected = "This shouldn't normally happen.";
286+ warn() << expected;
287+ ASSERT_STREQ(expected, str().c_str());
288+}
289+
290+TEST_F(LoggerTest, testCritical)
291+{
292+ auto expected = "Critical fail.";
293+ critical() << expected;
294+ ASSERT_STREQ(expected, str().c_str());
295+}
296+
297+TEST_F(LoggerTest, testError)
298+{
299+ auto expected = "Error crash.";
300+ ASSERT_EXIT({error() << expected;},
301+ ::testing::KilledBySignal(SIGTRAP), expected);
302+ ASSERT_STREQ(expected, str().c_str());
303+}
304+
305+} // namespace
306
307=== modified file 'test/headers/CMakeLists.txt'
308--- test/headers/CMakeLists.txt 2017-01-19 13:52:32 +0000
309+++ test/headers/CMakeLists.txt 2017-04-03 21:10:57 +0000
310@@ -13,6 +13,7 @@
311 set(exclusions
312 "GlibMemory.h"
313 "GObjectMemory.h"
314+ "Logger.h"
315 )
316
317 foreach(dir ${subdirs})
318
319=== modified file 'test/headers/includechecker.py'
320--- test/headers/includechecker.py 2017-01-19 13:52:32 +0000
321+++ test/headers/includechecker.py 2017-04-03 21:10:57 +0000
322@@ -38,6 +38,7 @@
323 'unity/shell': { 'Qt' }, # Anything under unity/shell can include anything starting with Qt
324 'unity/util/GObjectMemory': { 'glib' }, # The unity/util/GObjectMemory header can include anything starting with glib
325 'unity/util/GlibMemory': { 'glib' }, # The unity/util/GlibMemory header can include anything starting with glib
326+ 'unity/util/Logger': { 'glib' }, # The unity/util/Logger header requires glib
327 }
328
329 def check_file(filename, permitted_includes):

Subscribers

People subscribed via source and target branches

to all changes: