gabime / spdlog

Fast C++ logging library.
Other
24.27k stars 4.54k forks source link

HELP! Do not know how to use a custom sink for logging. #1668

Closed fahmidshibib closed 4 years ago

fahmidshibib commented 4 years ago

Hi, I am trying to create a custom sink that would target Oracle DB. I tried to follow the example of sqlite_lite and it is something I would like to use as format and replace the code to target Oracle DB. (link to sqlite_sink: https://github.com/PedroRod/sqlite_sink/blob/master/sqlite_sink.h)

Now the issues I am facing are (disclaimer: I am a total newbie at C++) 1) I do not know how to use the custom sink using a logger. Therefore, how do I create a logger that will use my sink to process the log message and send it to oracle? And where in the code do I create logger that uses my custom sink, in the main code or in the custom sink's header file? I tried the following codes (found it in the wiki) in the main code of the project where I created basic_logger_mt for testing spdlog: auto sink = std::make_shared(); auto my_logger = std::make_shared("mylogger", sink); I cannot test if this creates a logger using my custom sink because of the 2nd issue.

2) In one of my attempts I received the error in MS VS "spdlog::sinks::oracle_sink': cannot instantiate abstract class". I do not understand why that is happening and I cannot test my sink because of it.

image

3) On line 11 of the sqlite_sink example "class sqlite_sink : public sink" is being used whereas the spdlog wiki tells to use "class sqlite_sink : public basic_sink". Which on should I use for my custom function, 'sink' or 'basic_sink'.

4) I am aiming to use my custom sink to log details from various parts of a large project that contains multiple .cpp files. Therefore, I would also like to know if I have to make any additions to the sqlite_sink sink to make it capable of achieving this.

Thanks!

The custom sink I created by customizing the sqlite_sink if anyone wants to take a look at my failure till now

`using namespace oracle::occi; namespace spdlog { namespace sinks { using namespace oracle::occi; class oracle_sink : public sink { private: Environment env; Connection conn; Statement *stmt;

    public:
        explicit oracle_sink(const std::string& databaseName)
        {
            env = Environment::createEnvironment();

            //createConnection(username, password, connectionstring)
            conn = env->createConnection(password, login_name, targetdatabase);
            stmt = conn->createStatement("INSERT INTO EVENT_LOG VALUES (:1, :2, :3, :4, :5, :6, :7, :8)");

        }

        ~oracle_sink()
        {
            conn->terminateStatement(stmt);
            env->terminateConnection(conn);
            Environment::terminateEnvironment(env);
        }

        //void bind_to_statement(const details::log_msg& msg) const
        //{
        //  //getting time 
        //  stmt->setTimestamp(1, )

        //      //PID varchar(10 BYTE)
        //      stmt->setString(2, );

        //  //username varchar(256 BYTE)
        //  stmt->setString(3, );

        //  //Severity varchar(10 BYTE)
        //  stmt->setString(4, );

        //  //THREAD_NAME varchar(100 BYTE)
        //  stmt->setString(5, );

        //  //LOGGER varchar(10BYTE)
        //  stmt->setString(6, );

        //  //MESSAGE varchar(10BYTE)
        //  stmt->setString(7, );

        //  //PID CLOB
        //  stmt->setString(8, );

        /*}*/

        void log(const details::log_msg& msg) override
        {
            auto time = std::chrono::system_clock::to_time_t(msg.time);
            char time_str[26];
            ctime_s(time_str, sizeof(time_str), &time);

            //THREAD_NAME varchar(100 BYTE)
            stmt->setString(5, time_str);

            stmt->executeUpdate();

            /*sqlite3_reset(_query_stmt);
            sqlite3_clear_bindings(_query_stmt);*/
        }

    };
}

}`

tt4g commented 4 years ago
  1. On line 11 of the sqlite_sink example "class sqlite_sink : public sink" is being used whereas the spdlog wiki tells to use "class sqlite_sink : public basic_sink". Which on should I use for my custom function, 'sink' or 'basic_sink'.

Did you mean base_sink? I'll answer on that basis for now.

basic_sink extends sink. From an object-oriented point of view, they are both spdlog::sinks::sink. You may inherit from either class as long as you implement the necessary functions.

However, as far as I know, database connections are not thread-safe. In light of that, it is recommended to inherit from base_sink, because you can make the log output thread-safe with std::mutex.

  1. I am aiming to use my custom sink to log details from various parts of a large project that contains multiple .cpp files. Therefore, I would also like to know if I have to make any additions to the sqlite_sink sink to make it capable of achieving this.

The sqlite_sink is not thread-safe as far as I can see. If you are likely to use oracle_sink in a multi-thread, you should make it thread-safe with std::mutex and so on.

tt4g commented 4 years ago

It is recommended to refer to basic_file_sink and tcp_sink as reference implementations.

basic_file_sink:

tcp_sink: https://github.com/gabime/spdlog/blob/v1.8.0/include/spdlog/sinks/tcp_sink.h

tt4g commented 4 years ago

An example implementation that extends base_sink (it is recommended not to use the spdlog namespace.):

namespace spdlog
{
namespace sinks
{

using namespace oracle::occi;

template<typename Mutex>
class oracle_sink : public spdlog::sinks::base_sink<Mutex>
{
private:
    Environment *env;
    Connection *conn;
    Statement *stmt;

public:

    explicit oracle_sink(const std::string& databaseName)
    {
        env = Environment::createEnvironment();

        //createConnection(username, password, connectionstring)
        conn = env->createConnection(password, login_name, targetdatabase);
        stmt = conn->createStatement("INSERT INTO EVENT_LOG VALUES (:1, :2, :3, :4, :5, :6, :7, :8)");
    }

    ~oracle_sink()
    {
        conn->terminateStatement(stmt);
        env->terminateConnection(conn);
        Environment::terminateEnvironment(env);
    }

    oracle_sink(const &oracle_sink) = delete;

    oracle_sink(oracle_sink&&) = delete;

protected:

    void sink_it_(const spdlog::details::log_msg &msg) override
    {
        bind_to_statement(msg);

        stmt->executeUpdate();
    }

    void flush_() override
    {
        // Do nothing because statement executed in sink_it_().
    }

    void set_pattern_(const std::string &pattern) override
    {
        // Don't format log message.
    }

    void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter) override
    {
        // Don't format log message.
    }

private:

    //void bind_to_statement(const details::log_msg& msg) const
    //{
    //    //getting time 
    //    stmt->setTimestamp(1, )

    //    //PID varchar(10 BYTE)
    //    stmt->setString(2, );

    //    //username varchar(256 BYTE)
    //    stmt->setString(3, );

    //    //Severity varchar(10 BYTE)
    //    stmt->setString(4, );

    //    //THREAD_NAME varchar(100 BYTE)
    //    stmt->setString(5, );

    //    //LOGGER varchar(10BYTE)
    //    stmt->setString(6, );

    //    //MESSAGE varchar(10BYTE)
    //    stmt->setString(7, );

    //    //PID CLOB
    //    stmt->setString(8, );

    /*}*/

};

using oracle_sink_mt = oracle_sink<std::mutex>;

} // namespace sinks
} // namespace spdlog

Since you don't need to format the message, set_pattern_() and set_formatter_() do nothing.

You can use it:

auto sink = std::make_shared<spdlog::sinks::oracle_sink_mt>(databasename);
auto my_logger = std::make_shared<spdlog::logger>("mylogger", sink);
fahmidshibib commented 4 years ago

Thanks a lot for your help! You helped a lot! I had to add template<typename Mutex> before class oracle_sink : public spdlog::sinks::base_sink<Mutex> or else I get the error that Mutex identifier is undefined.

A have a follow up question though: In basic_file_sink.h, they use the following code in order to create the logger using their own base_file_sink i guess.. template<typename Factory = spdlog::synchronous_factory> inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate = false) { return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate); } (lines 145 to 149 in basic_file_sink.h https://github.com/gabime/spdlog/blob/v1.8.0/include/spdlog/sinks/basic_file_sink.h) So do I need to use this as well? If so, should I keep the arguments in basic_logger_mt() the same when I replace it with the name of the custom logger I am trying to create? Because eventually I would like to add arguments such as password, login, and database name only as arguments instead of file name.

Also, does this following code serve the same purpose as the code mentioned above to create my custom logger? auto sink = std::make_shared<spdlog::sinks::oracle_sink_mt>(databasename); auto my_logger = std::make_shared<spdlog::logger>("mylogger", sink); Can you tell me what 'databasename' argument represents and what it is being used for? Can I use this argument space to take arguments for my pass, login, etc for oracle?

I also noticed that the code for sqlite_sink used the following code. Is it creating the sink or the logger for the sink? auto logging = spdlog::create<spdlog::sinks::database_logger_sink>("SQLiteSink","database.db"); Can you explain what the arguments are being used for here, I dont see reference to these arguments in the sqlite_sink. (Reference to sqlite_sink: https://github.com/PedroRod/sqlite_sink)

Now I have three different codes to create a multithread capable logger. One from reference from basic_file_sink.h, one from the spdlog wiki, and the last one from reference from sqlite_sink. Can you please suggest me which is the best option if I want to achieve multi-thread logging?

Some extra stupid questions: 1) When I finally create this sink and a logger to use it, will the sink automatically log data to my database each time I write spdlog::info("blah blah") ? Or do I need to add some more code for the log to actually push through? I ask this because my sink uses the information from spdlog::details:log_msg to create the log. Does log_msg get updated and hence used by my sink each time I call spdlog::info ? This method of getting logging data was used in sqlite_sink which kind of makes sense.

2) In my original post in issue 2 I had the error 'cannot instantiate abstract class'. Did you address this error by overriding flush(), set_pattern(), set_formatter() with empty definitions so that there is longer any pure virtual methods to make my custom sink an abstract class?

P.S: Thank you so very much for your immense help and patience! I would also request any moderator to not close this thread yet as I might have follow up questions as I progress with my attempt to target Oracle DB using spdlog, and I guess it will also help people who are lost like me (or maybe I am the most lost person).

tt4g commented 4 years ago

Oops! I'm forget template<typename Mutex>. I have corrected my previous comment.

template<typename Factory = spdlog::synchronous_factory>

This is option. it is shorthand create sink and logger.

Example:

namespace spclog
{

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> oracle_sink_mt(const std::string &logger_name, const std::string &databaseName)
{
    return Factory::template create<sinks::oracle_sink_mt>(logger_name, databaseName);
}

} // namespace spclog

// Usage
std::shared_ptr<spdlog::logger> mylogger = spdog::oracle_sink_mt("mylogger", databaseName);

Also, does this following code serve the same purpose as the code mentioned above to create my custom logger?

Yes.

Can you tell me what 'databasename' argument represents and what it is being used for? Can I use this argument space to take arguments for my pass, login, etc for oracle?

std::make_shared<T> takes constructor argument of T: https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared That is, databasename is constructor argument of oracle_sink_mt.

tt4g commented 4 years ago

I also noticed that the code for sqlite_sink used the following code. Is it creating the sink or the logger for the sink? auto logging = spdlog::create("SQLiteSink","database.db");

it gives the same result as spdog::oracle_sink_mt("mylogger", databaseName); above example. This is declare here: https://github.com/gabime/spdlog/blob/4a9ccf7e38e257feecce0c579a782741254eaeef/include/spdlog/spdlog.h#L33-L37

Can you explain what the arguments are being used for here, I dont see reference to these arguments in the sqlite_sink. (Reference to sqlite_sink: PedroRod/sqlite_sink)

Since spdlog::create is implemented by a C++ template function, the The arguments passed are passed to the logger and sink. The first argument is the name of the logger. "database.db" is the databaseName argument of sqlite_sink.

tt4g commented 4 years ago
  1. When I finally create this sink and a logger to use it, will the sink automatically log data to my database each time I write spdlog::info("blah blah") ? Or do I need to add some more code for the log to actually push through? I ask this because my sink uses the information from spdlog::details:log_msg to create the log. Does log_msg get updated and hence used by my sink each time I call spdlog::info ? This method of getting logging data was used in sqlite_sink which kind of makes sense.

The logger does some level-based filtering before passing the message to sink. Call spdlog::logger::set_level(spdlog::level::trace) if you want to pass the entire log to sink.

When a sink writes the log to the database depends on the implementation of the sink. Many implementations buffer the log and write the buffer when spdlog::sinks::sink::flush() is called. However, it's difficult to store database statements in a buffer, so you should write to the database every time sink_it_(const spdlog::details::log_msg &msg) is called.

  1. In my original post in issue 2 I had the error 'cannot instantiate abstract class'. Did you address this error by overriding flush(), set_pattern(), set_formatter() with empty definitions so that there is longer any pure virtual methods to make my custom sink an abstract class?

Right.

spdlog::sinks::sink declare pure virtual log(), flush(), set_pattern() and set_formatter(). Custom sinks must override all of these.

https://github.com/gabime/spdlog/blob/4a9ccf7e38e257feecce0c579a782741254eaeef/include/spdlog/sinks/sink.h#L16-L19

fahmidshibib commented 4 years ago

Hi, thank you so much for the feedback.

oracle_sink(const &oracle_sink) = delete;
oracle_sink(oracle_sink&&) = delete;

this part of the code causes this error image

Can you also please explain what this piece of code does? Thanks!

tt4g commented 4 years ago

Sorry, that is typo. Replace oracle_sink(const &oracle_sink) = delete; with oracle_sink(const oracle_sink&) = delete;

Diff:

- oracle_sink(const &oracle_sink) = delete;
+ oracle_sink(const oracle_sink&) = delete;
fahmidshibib commented 4 years ago

Thanks. It built without error! However I have some follow up questions. This is the sink code I have setup to test if the sink is actually running. So I have inserted test statements to oracle in the sink and the main code. In the sink put it in the constructor and bind_to_statement() method (in order to test sinkit() method as binding statement method is called through it):

`namespace spdlog { namespace sinks {

    using namespace oracle::occi;
    template<typename Mutex>
    class oracle_sink : public spdlog::sinks::base_sink<Mutex>
    {
    private:
        Environment *env;
        Connection *conn;
        Statement *stmt;

    public:

        explicit oracle_sink(const std::string& databaseName)
        {
            Environment *env = Environment::createEnvironment();

            {

                Connection *conn = env->createConnection(username, pass, target);
                Statement *stmt = conn->createStatement("INSERT INTO EVENT_LOG VALUES (NULL, NULL, NULL, NULL, NULL, NULL, 'testsinkconst', NULL)");
                stmt->executeUpdate();
                conn->terminateStatement(stmt);
                env->terminateConnection(conn);
            }

            Environment::terminateEnvironment(env);
        }

        ~oracle_sink()
        {
            conn->terminateStatement(stmt);
            env->terminateConnection(conn);
            Environment::terminateEnvironment(env);
        }

        oracle_sink(const oracle_sink&) = delete;

        oracle_sink(oracle_sink&&) = delete;

    protected:

        void sink_it_(const spdlog::details::log_msg &msg) override
        {
            bind_to_statement(msg);

            stmt->executeUpdate();
        }

        void flush_() override
        {
            spdlog::drop_all();
        }

        void set_pattern_(const std::string &pattern) override
        {
            // Don't format log message.
        }

        void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter) override
        {
            // Don't format log message.
        }

    private:

        void bind_to_statement(const details::log_msg& msg) const
        {
            auto time = std::chrono::system_clock::to_time_t(msg.time);
            char time_str[26];
            ctime_s(time_str, sizeof(time_str), &time);

            //THREAD_NAME varchar(100 BYTE)
            //stmt->setString(5, time_str);
            Environment *env = Environment::createEnvironment();

            {

                Connection *conn = env->createConnection(username, pass, target);
                Statement *stmt = conn->createStatement("INSERT INTO EVENT_LOG VALUES (NULL, NULL, NULL, NULL, NULL, NULL, 'testbind2st', NULL)");
                stmt->executeUpdate();
                conn->terminateStatement(stmt);
                env->terminateConnection(conn);
            }

            Environment::terminateEnvironment(env);
        /*  stmt->executeUpdate();*/
        } //bind_to_statement

    };

    using oracle_sink_mt = oracle_sink<std::mutex>;

} // namespace sinks

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> oracle_logger_mt(const std::string &logger_name, const std::string &databaseName)
{
    return Factory::template create<sinks::oracle_sink_mt>(logger_name, databaseName);
}

} // namespace spdlog`

As you can see from this resulting log in oracle DB image

The statements from the main code and the constructor are logged but not from the sinkit() method. However, all the main logging usually takes place in the sinkit() method (or log() method for sqlite_sink) and only the statement preparation takes place in the sink constructor. If the method responsible for running the logging statements does not execute how does the logging take place? I must be definitely missing something. Referencing this from the sqlite_sink database sink.

Also, this is a snippet of my main code if you could kindly check if I am using my custom sink and logger properly: image

Thanks!

tt4g commented 4 years ago

but not from the sinkit() method.

Do not call spdlog::drop_all() with flush_(). The operation will remove all loggers in the global registry of spdlog.

And you seem to be using the Oracle library and writing C++ incorrectly.

In the constructor, Connection *conn = env->createConnection(username, pass, target); assigns the created connection to the local variable conn. Even stmt you are manipulating a local variable, not a member variable.

The same code exists in bind_to_statement().

At least the member variables conn and stmt are uninitialized in the sink_it_() function. If the destructor is called in this state, it will cause undefined behavior.

This problem should be fixed by your familiarity with Oracle library usage and C++.

tt4g commented 4 years ago

Also, this is a snippet of my main code if you could kindly check if I am using my custom sink and logger properly:

spdlog::info("AscendServices Intiated") will not use oracle_sink. Because spdlog::info("AscendServices Intiated") to be used default logger. default logger is output console only (by default).

You can change default logger with spdlog::set_default_logger() function.

Wiki will help you: https://github.com/gabime/spdlog/wiki/Default-logger

Example:

std::shared_ptr<spdlog::sinks::sink> basic_file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("C://Tools//ProjectServer//test-log-1.txt");
std::shared_ptr<spdlog::sinks::sink> oracle_sink =std::make_shared<spdlog::sinks::oracle_sink_mt>("//crp30777:1521/xe");

std::vector<std::shared_ptr<spdlog::sinks::sink>> myLoggerSinks;
myLoggerSinks.push_back(basic_file_sink);
myLoggerSinks.push_back(oracle_sink):

// Creating loggers with multiple sinks: https://github.com/gabime/spdlog/wiki/2.-Creating-loggers#creating-loggers-with-multiple-sinks
std::shared_ptr<spdlog::logger> myLogger = std::make_shared<spdlog::logger("myLogger", myLoggerSinks.begin(), myLoggerSinks.end());
spdlog::register_logger(myLogger);

// Change default logger.
/// spdlog::register_logger(myLogger); // EDITIED
spdlog::set_default_logger(myLogger); // EDITIED

// Output this log message to basic_file_sink and oracle_sink.
spdlog::info("AscendServices Intiated";
fahmidshibib commented 4 years ago

Hi thank you so much for your help! I tried what you suggested and read a bit more about the basics of the OCCI library and made some adjustments to the sink. However, after adding what you suggested I got this exception pop up while debugging. Initially I thought it could be a mistake I made setting up the occi codes, however, removing the occi codes did not help. Then I guessed the issue maybe that you defined the variables basic_file_sink and oracle_sink again while creating a logger with multiple sinks so I moved their definitions in the myLoggerSinks vector directly. But none of that helped and this exception still exists.

image

As of last run, the spdlog section of the main code looks like this:

`//spdlog testing /std::shared_ptr basic_file_sink = std::make_shared("C://Tools//ProjectServer//test-log-1.txt"); std::shared_ptr oracle_sink = std::make_shared("//crp30777:1521/xe");/

std::vector<std::shared_ptr<spdlog::sinks::sink>> myLoggerSinks;
myLoggerSinks.push_back(std::make_shared<spdlog::sinks::oracle_sink_mt>("//crp30777:1521/xe"));
myLoggerSinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("C://Tools//ProjectServer//test-log-1.txt"));

// Creating loggers with multiple sinks
std::shared_ptr<spdlog::logger> myLogger = std::make_shared<spdlog::logger>("myLogger", myLoggerSinks.begin(), myLoggerSinks.end());
spdlog::register_logger(myLogger);

// Change default logger.
spdlog::register_logger(myLogger);

// Output this log message to basic_file_sink and oracle_sink.
spdlog::info("AscendServices Intiated");`

And the custom sink code looks like this:

`

pragma once

include "spdlog\sinks\sink.h"

include "spdlog/details/log_msg.h"

include "spdlog/common.h"

include <spdlog/details/file_helper.h>

include <spdlog/details/null_mutex.h>

include <spdlog/sinks/base_sink.h>

include <spdlog/details/synchronous_factory.h>

include

include

include

namespace spdlog { namespace sinks {

    using namespace oracle::occi;
    template<typename Mutex>
    class oracle_sink : public spdlog::sinks::base_sink<Mutex>
    {
    private:
        Environment *env;
        Connection *conn;
        Statement *stmt;

    public:

        explicit oracle_sink(const std::string& databaseName)
        {
            env = Environment::createEnvironment();

            conn = env->createConnection("LOGGING_DEV", "LOGGING_DEV", "//crp30777:1521/xe");
            stmt = conn->createStatement();
            /*stmt->setSQL("INSERT INTO EVENT_LOG VALUES(:1, :2, :3, :4, :5, :6, :7, :8)");*/
            conn->terminateStatement(stmt);
            env->terminateConnection(conn);
            Environment::terminateEnvironment(env);
        }

        ~oracle_sink()
        {

        }

        oracle_sink(const oracle_sink&) = delete;

        oracle_sink(oracle_sink&&) = delete;

    protected:

        void sink_it_(const spdlog::details::log_msg &msg) override
        {
            bind_to_statement(msg);
            stmt->executeUpdate();

        }

        void flush_() override
        {
            //empty
        }

        void set_pattern_(const std::string &pattern) override
        {
            // Don't format log message.
        }

        void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter) override
        {
            // Don't format log message.
        }

    private:

        void bind_to_statement(const details::log_msg& msg) const
        {
            auto time = std::chrono::system_clock::to_time_t(msg.time);
            char time_str[26];
            ctime_s(time_str, sizeof(time_str), &time);

            //stmt->setString(7, time_str);
        }
    };

    using oracle_sink_mt = oracle_sink<std::mutex>;

} // namespace sinks

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> oracle_logger_mt(const std::string &logger_name, const std::string &databaseName)
{
    return Factory::template create<sinks::oracle_sink_mt>(logger_name, databaseName);
}

} // namespace spdlog`

And I am guessing now that you have created a logger with dual sinks I no longer need this piece of code at the end of the sink: template<typename Factory = spdlog::synchronous_factory> inline std::shared_ptr<logger> oracle_logger_mt(const std::string &logger_name, const std::string &databaseName) { return Factory::template create<sinks::oracle_sink_mt>(logger_name, databaseName); }

But would keeping it be an issue?

Thanks again for all your help!

fahmidshibib commented 4 years ago

I think the exception was due my attempt of creating the logger with multiple sinks. Because when I use the following code in main to use my custom logger with custom sink only the exception is no longer thrown. std::shared_ptr<spdlog::logger> mylogger = spdlog::oracle_logger_mt("mylogger", "databaseName"); spdlog::set_default_logger(mylogger);

tt4g commented 4 years ago

I think the exception was due my attempt of creating the logger with multiple sinks. Because when I use the following code in main to use my custom logger with custom sink only the exception is no longer thrown.

Right, it's my BAD. I made a mistake again with the Example code (sorry...).

Fixed the previous sample code. An error occurs when calling spdlog::register_logger(myLogger); twice. Replace the second call to spdlog::register_logger(myLogger); with spdlog::set_default_logger(myLogger).

When you write the code, could you put it in a markdown source format?

Example:

```cpp
#include <iostream>

int main()
{
    std::cout << "Hello, Wandbox!" << std::endl;
}

Output:

```cpp
#include <iostream>

int main()
{
    std::cout << "Hello, Wandbox!" << std::endl;
}

If I'm checking unformatted code, I have to go back and forth to the comment entry area on GitHub and I make typing mistakes.

fahmidshibib commented 4 years ago

Sorry I did not know about that and simply used the "insert code" functionality! Thanks, the logger worked! I was wondering if I wanted to use this same logger on another cpp file within the same project from inside a method definition I could use the spdlog::get() method right?

Also, now that the sink is setup I wanted to use the data that spdlog collects to create a log to create a log of my own on Oracle DB. Referring back to the sqlite_sink, it used the data from details::log_msg to create its logs for sqlite as shown below:

void bind_to_statement(const details::log_msg& msg) const
            {
                auto time = std::chrono::system_clock::to_time_t(msg.time);

                char time_str[26];

                ctime_s(time_str, sizeof(time_str), &time);

                if (sqlite3_bind_text(_query_stmt, 1, time_str, -1, SQLITE_STATIC) != SQLITE_OK ||
                    sqlite3_bind_text(_query_stmt, 2, to_str(msg.level), -1, SQLITE_STATIC) != SQLITE_OK ||
                    sqlite3_bind_text(_query_stmt, 3, msg.raw.c_str(), -1, SQLITE_STATIC) != SQLITE_OK ||
                    sqlite3_bind_text(_query_stmt, 4, msg.logger_name.c_str(), -1, SQLITE_STATIC) != SQLITE_OK ||
                    sqlite3_bind_int(_query_stmt, 5, msg.thread_id) != SQLITE_OK)
                    throw spdlog_ex(sqlite3_errmsg(_database));
            }

However, when I try to use the codes to_str(msg.level) it does not work. I am guessing to_str() is a sqlite method and not a c++ method. And level is defined as an enum in log_msg, so I was wondering how I could use that as string because I need my argument to be a string. For a bit more clarity on why I need the information in string format, OCCI has a method as follows: stmt->setString(1, time_str); where the first argument refers to the column I am updating and the second argument is the string I want to store in the associated column of the database table (given that the column is setup to accept strings, which is true for my case).

Also, observing the five columns the sqlite_sink is updating, I figured msg.raw.c_str() would be the one storing the actual message text. However, when I look at the definition of log_msg, there is nothing called raw and I also get an error "'raw' is not a member of 'spdlog::details::log_msg'". Bellow I have included the definition of the log_msg struct

namespace details {
struct SPDLOG_API log_msg
{
    log_msg() = default;
    log_msg(log_clock::time_point log_time, source_loc loc, string_view_t logger_name, level::level_enum lvl, string_view_t msg);
    log_msg(source_loc loc, string_view_t logger_name, level::level_enum lvl, string_view_t msg);
    log_msg(string_view_t logger_name, level::level_enum lvl, string_view_t msg);
    log_msg(const log_msg &other) = default;

    string_view_t logger_name;
    level::level_enum level{level::off};
    log_clock::time_point time;
    size_t thread_id{0};

    // wrapping the formatted text with color (updated by pattern_formatter).
    mutable size_t color_range_start{0};
    mutable size_t color_range_end{0};

    source_loc source;
    string_view_t payload;
};
} // namespace details
} // namespace spdlog

So how would I be able to log the actual log text message?

Eventually my goal is to collect the following information in the following datatype formats as in the brackets: Event_Time (timestamp(6)) Process ID (string) Level (string) Thread_name (string) logger (string) message (string) Exception (CLOB)

How many of these can be actually collected using spdlog and how do you suggest I collect these in their associated formats?

Thanks for all your detailed help!

tt4g commented 4 years ago

if I wanted to use this same logger on another cpp file within the same project from inside a method definition I could use the spdlog::get() method right?

Yes. You can get your logger by spdlog::get(). Remember that spdlog::get() has to pass the name of the logger passed to spdlog::register_logger() as an argument. In your previous snippet, "myLogger" was the name of the logger.

tt4g commented 4 years ago

However, when I try to use the codes to_str(msg.level) it does not work. I am guessing to_str() is a sqlite method and not a c++ method. And level is defined as an enum in log_msg, so I was wondering how I could use that as string because I need my argument to be a string.

C++ enum is the same as int in most cases (NOTE that this is Lie-to-children. Please refer to the references for a proper understanding of enum: https://en.cppreference.com/w/cpp/language/enum).

In C++, it is also the programmer's responsibility to convert enum and int to std::string and char *. If you want std::string of spdlog::level::level_enum, you should define the conversion function yourself.

Example: ```cpp #include namespace spdlog { namespace level { enum level_enum { trace = 0, debug = 1, info = 2, warn = 3, err = 4, critical = 5, off = 6, n_levels }; } // enum level_enum } // namespace spdlog std::string to_str(spdlog::level::level_enum lvl) { switch (lvl) { case spdlog::level::level_enum::trace: return "trace"; case spdlog::level::level_enum::debug: return "debug"; case spdlog::level::level_enum::info: return "info"; case spdlog::level::level_enum::warn: return "warn"; case spdlog::level::level_enum::err: return "err"; case spdlog::level::level_enum::critical: return "critical"; case spdlog::level::level_enum::off: return "off"; } return "unknown"; } int main() { std::cout << "trace: " << to_str(spdlog::level::level_enum::trace) << std::endl; std::cout << "debug: " << to_str(spdlog::level::level_enum::debug) << std::endl; std::cout << "info: " << to_str(spdlog::level::level_enum::info) << std::endl; std::cout << "warn: " << to_str(spdlog::level::level_enum::warn) << std::endl; std::cout << "err: " << to_str(spdlog::level::level_enum::err) << std::endl; std::cout << "critical: " << to_str(spdlog::level::level_enum::critical) << std::endl; std::cout << "off: " << to_str(spdlog::level::level_enum::off) << std::endl; // C++ enum is unsafe. std::cout << "Oops! " << to_str(static_cast(-1)) << std::endl; } ```

Run online: https://wandbox.org/permlink/8MvH4jdrzK36h2lm

fahmidshibib commented 4 years ago

Yes. You can get your logger by spdlog::get(). Remember that spdlog::get() has to pass the name of the logger passed to spdlog::register_logger() as an argument. In your previous snippet, "myLogger" was the name of the logger.

Thanks for the tip. I also noticed that in the wiki and other forum examples.

Also a small update on the logger "myLogger" that contains both the sinks oracle_sink_mt (for targeting oracle db) and basic_file_sink_mt (for logging to data): the text file is created by basic_file_sink_mt but it does not actually log anything to the text file. My current usage in the main code looks like this:

        std::vector<std::shared_ptr<spdlog::sinks::sink>> myLoggerSinks;
    myLoggerSinks.push_back(std::make_shared<spdlog::sinks::oracle_sink_mt>("Blackbird"));
    myLoggerSinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("C://Tools//ProjectServer//test-log-2.txt"));

    // Creating loggers with multiple sinks
    std::shared_ptr<spdlog::logger> myLogger = std::make_shared<spdlog::logger>("myLogger", myLoggerSinks.begin(), myLoggerSinks.end());
    spdlog::register_logger(myLogger);

    /* Change default logger.*/
    spdlog::set_default_logger(myLogger);

    // Output this log message to basic_file_sink and oracle_sink.
    spdlog::info("AscendServices Intiated");

It neither give any error nor does anything weird. It simply creates the file only and does nothing. I am guessing if the actual writing to the file is done by the logger basic_logger_mt (provided below) that was created in basic_file_sink.h right with the definition of basic_file_sink. But I suppose that would be undermining the purpose of the sink itself so I may be wrong.

template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate = false)
{
    return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate);
}

It would be nice to be able to log to a text file as well, but my higher priority for now is the issues I mentioned in the last comment associated with collecting various logging information from spdlog to push to oracle db.

tt4g commented 4 years ago

Event_Time (timestamp(6))

Get from spdlog::details::log_msg::time.

NOTE: The type of spdlog::details::log_msg::time is std::chrono::system_clock::time_point: https://github.com/gabime/spdlog/blob/4a9ccf7e38e257feecce0c579a782741254eaeef/include/spdlog/common.h#L97

Unfortunately, the ability to convert std::chrono::system_clock::time_point to a string or a specific clock time does not exist in C++ (It will be added in C++ 20). You must convert the std::chrono::system_clock::time_point by your own hand so that it can be registered as an Oracle timestamp.

If Oracle timestamp accepts string format, you can use fmt library that is bundled in spdlog.

Convert std::chrono::system_clock::time_point to string example with fmt :

#include <chrono>

#include "spdlog/fmt/chrono.h"

int main() {
    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
    // Convert to time_t type.
    auto time_t_now = std::chrono::system_clock::to_time_t(now);

    fmt::print("format: {:%Y-%m-%d %H:%M:%S}\n", fmt::localtime(time_t_now)); // Outputs: "format: 2020-09-11 04:22:48"
}

Online run result: https://godbolt.org/z/79z596

See also:

tt4g commented 4 years ago

Process ID (string)

spdlog::details::log_msg does not have PID information.

There is no function defined in the C++ STL (standard function) that provides PID. You have to call an OS-provided function to get the PID (i.e., the way to get the PID depends on the OS and other environment).

For example, on a UNIX-like OS, you can get the PID with the getpid() function.

spdlog implementation here: https://github.com/gabime/spdlog/blob/4a9ccf7e38e257feecce0c579a782741254eaeef/include/spdlog/details/os-inl.h#L383-L391

tt4g commented 4 years ago

Level (string)

See my previous comment: https://github.com/gabime/spdlog/issues/1668#issuecomment-690858165

Thread_name (string)

Get from spdlog::details::log_msg::thread_id.

logger (string)

Get from `spdlog::details::log_msg::logger_nameP.

message (string)

Get from spdlog::details::log_msg::payload.

tt4g commented 4 years ago

Exception (CLOB)

spdlog::details::log_msg does not have exception.

Perhaps you are aware that logger libraries in other programming languages accept an exception object as part of their arguments. And you'd expect spdlog to accept an exception object as an argument to the log function as well. However, spdlog does not accept exceptions in the log function.

std::exception is defined as the exception to be throw in the C++ library. almost all objects in C++ are throwable: https://en.cppreference.com/w/cpp/language/exceptions For example, throw std::string("foo") and throw 100 are also possible.

This means that C++ does not have a specific type of exception. spdlog cannot distinguish between an exception object and an object to be included in a log message in the log function based on the type of the argument. Therefore, there is no exception information in spdlog::details::log_msg.

If you want to handle errors that occur inside spdlog, you can register an error handler: https://github.com/gabime/spdlog/wiki/Error-handling

tt4g commented 4 years ago

Reply to: https://github.com/gabime/spdlog/issues/1668#issuecomment-690858297

If you are not called spdlog::shutdown() before application exit, log data may not have been written to disk from the memory buffer.

Try call spdlog::flush_on(spdlog::level::trace). Another way, call spdlog::shutdown() just before exit.

Wiki; https://github.com/gabime/spdlog/wiki/7.-Flush-policy

fahmidshibib commented 4 years ago

Hi, never mind my previous comment. I deleted it because I understood where I made the mistake in defining your example function with switch statement.

Also, thanks for your so detailed replies. It's super helpful for me as I am super new to all these. I'll leave you feedback as I figure out the rest of the stuff using your tips. Update till now: getpid() is the way to go to PID in C++ and your example function worked perfectly to convert the enum to string and eventually log it in Oracle DB.

Thanks!

fahmidshibib commented 4 years ago

Level (string)

See my previous comment: #1668 (comment)

Thread_name (string)

Get from spdlog::details::log_msg::thread_id.

logger (string)

Get from `spdlog::details::log_msg::logger_nameP.

message (string)

Get from spdlog::details::log_msg::payload.

thread_id, logger_name, and payload; non of these are strings hence I get errors when I try using them as arguments. I suppose these are different datatypes that need to be converted to strings. Any suggestion on how I can convert them to strings?

Thanks

tt4g commented 4 years ago

thread_id, logger_name, and payload; non of these are strings hence I get errors when I try using them as arguments. I suppose these are different datatypes that need to be converted to strings. Any suggestion on how I can convert them to strings?

It's easy to use the fmt library:

#include "spdlog/fmt/fmt.h"

// {:d} is decimal integer dyntax: https://fmt.dev/latest/syntax.html
std::string thread_id_str = fmt::format("{:d}", thread_id);
std::string logger_name_str = fmt::format("{}", logger_name);
std::string payload_str = fmt::format("{}", payload);
hoangbac2004 commented 3 months ago

I meet some issue, I have already install full Oracle 21c, but I have error in use occi.h

include

include

using namespace oracle::occi;

int main(int argc, char *argv[]) { auto env = Environment::createEnvironment(); auto conn = env->createConnection("system", "0", "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=XE)))");

auto stmt = conn->createStatement("INSERT INTO BLAH (DATA) VALUES (:1)");

stmt->execute();
conn->commit();
return 0;

}

image

if i use try, it throw exception(invalid username/password), if i use OCI, it it normal, how can't i fix this,