Stackify with POCO C++

Stackify with POCO C++

I don't really know how popular the POCO logging framework is, but it is my favorite.  I've used log4cpp, log4cxx, boost logging, easylogging++, and many more, but for documentation, ease of use, flexibility and developer maintenance I think it should be at the top of the list.

Getting Started with POCO

Before you can use POCO logging you need to get POCO binaries.  You can compile them from source as I have done, or you can use the Conan Package Manager Conan.

If you want the pre-built binaries for this sample project you can download them from my GitHub repository POCO Sources and POCO Binaries.  However, with this being a blog that includes security practices you probably shouldn't trust pre-built anything from a blog, and I would recommend you build them from source yourself or get Conan setup.

POCO allows you to extend the logging framework by creating a new channel for it to send log information.  The complete code can be found here Sample Code

Stack Traces

Getting stack traces in C++ is kind of an odd thing.  Call stacks require demangling and I tried experimenting with different techniques in these examples.  With POCO I used NDC or the (Network Diagnostic Context) which is kind of a trace statement that is dumped into the log you can send to Stackify.  In the other examples I've been using boost StackTrace which I like more than NDC, but I wanted to keep boost out of this sample.  From my experience people are either using boost or POCO right now as they do a lot of the same things.  I am currently thinking of adding this as a configurable parameter in the future.

Logging with POCO

POCO has a very flexible and easy to use logging framework that allow you to log to multiple data sources.  To enable logging to stackify I have just created another logging channel that we can tell the logger to send messages to.  A complete overview of the POCO logging framework can be found here POCO Logging Tutorial.

To start logging with Stackify import the library or copy the source into your own project and follow these steps

  • Get a smart pointer to the Stackify Channel
Poco::AutoPtr<DefenseAgainstTheDarkArts::StackifyChannel> p_stackify(new DefenseAgainstTheDarkArts::StackifyChannel);
  • Stackify requires a number of properties to be set for the messages to be logged and formatted properly.  The sample code includes helpers to get some of this information on Windows.
//Set Stackify Api Key (Required)
p_stackify->setProperty("apiKey", "Your API Key Here");

//Set Environment (Required
p_stackify->setProperty("environment", "development");

//Set Server/Computer Name (Required)
std::wstring local_computer_name = GetLocalComputerName();
const std::string server_name(local_computer_name.begin(), local_computer_name.end());
p_stackify->setProperty("serverName", server_name);

//Set Application Name Property (Required)
p_stackify->setProperty("appName", "Telemetry.Test");

//Set Application Location (Optional)
p_stackify->setProperty("appLocation", "C:\\Source\\GitHub\\NerdPlanet.Services\\Telemetry.Test\\Win32\\Debug");

//Set Logger information (Required) - Name it whatever you want
p_stackify->setProperty("logger", "defense-against-the-dark-arts-stackify-logger-pococpp-1.0.0.0");
	
//Set Platform Language (Optional)
p_stackify->setProperty("platform", "cpp");

//Set Operating System
p_stackify->setProperty("operatingSystem", "Windows");

  • We want to run our logger on a background thread so it is non-blocking. The Poco::AsyncChannel manages this for us by adding the stackify channel to the async channel.
Poco::AutoPtr<Poco::AsyncChannel> p_async(new Poco::AsyncChannel(p_stackify));
  • We are just going to set the async channel as the root channel here.  For logging to multiple channels please see the POCO logger documentation noted above.
Poco::Logger::root().setChannel(p_async);
  • Name the Logger and log some messages
Poco::Logger& logger = Poco::Logger::get("Telemetry.Sample");
	
//Log some simple messages.
for (int i = 0; i < 10; ++i)
{
    std::stringstream message_info;
    message_info << "Information #" << i;
    logger.information(message_info.str().c_str());
}

Well that about does it for this post.  The sample contains detailed information about logging exceptions as well, but hopefully this gets your started and looking at logging as a service.


StackifyChannel.h

#pragma once
#include <Poco/Channel.h>
#include <Poco/Message.h>
#include <Poco/JSON/Object.h>
#include <Poco/Net/Context.h>
#include <Poco/Net/InvalidCertificateHandler.h>
#include <Poco/Net/PrivateKeyFactory.h>


namespace DefenseAgainstTheDarkArts {


	class StackifyChannel : public Poco::Channel
		/// A channel that writes to an ostream.
		///
		/// Only the message's text is written, followed
		/// by a newline.
		///
		/// Chain this channel to a FormattingChannel with an
		/// appropriate Formatter to control what is contained 
		/// in the text.
		///
		/// Similar to StreamChannel, except that a static
		/// mutex is used to protect against multiple
		/// console channels concurrently writing to the
		/// same stream.
	{
	public:
		StackifyChannel();
		/// Creates the channel and attaches std::clog.

		StackifyChannel(std::ostream& str);
		/// Creates the channel using the given stream.

		void log(const Poco::Message& msg) override;
		/// Logs the given message to the channel's stream.


		void setProperty(const std::string& name, const std::string& value) override;
		/// Sets the property with the given name. 
		/// 
		/// The following properties are supported:
		///   * apiKey:       Your Stackify API Key (Required)
		///
		///   * environment:  The Environment you are logging to (Required)
		///
		///   * serverName:   Server or Device Name (Required)
		///                   
		///   * appName:      The name of the application (Required)
		///                   
		///   * location      The directory path of the application (Optional)
		///
		///   * logger:   Name and version of the logger (example: stackify-log-log4j12-1.0.12)
		///				      Required by the api, but defaults to stackify-log-pococpp1.0.0.0 if not set.
		///
		///   * platform:     The logging language (development language) - Defaults to CPP


		std::string getProperty(const std::string& name) const override;
		/// Returns the value of the property with the given name.
		/// See setProperty() for a description of the supported
		/// properties.

		static const std::string PROP_APIKEY;
		static const std::string PROP_ENVIRONMENT;
		static const std::string PROP_SERVERNAME;
		static const std::string PROP_APPNAME;
		static const std::string PROP_APPLOC;
		static const std::string PROP_LOGGER;
		static const std::string PROP_PLATFORM;
		static const std::string PROP_OPERATINGSYSTEM;
		static const std::string PROP_OSMAJORVERSION;
		static const std::string PROP_OSMINORVERSION;
		static const std::string PROP_TIMEZONE;

	protected:
		~StackifyChannel();

	private:
		std::ostream& str_;
		Poco::FastMutex mutex_;

		std::string api_key_;
		std::string environment_;
		std::string server_name_;
		std::string app_name_;
		std::string location_;
		std::string logger_;
		std::string platform_;
		std::string operating_system_;
		std::string os_major_version_;
		std::string os_minor_version_;
		std::string timezone_;

		std::string stackify_logging_save_log_url;

		std::string base_uri_;
		Poco::SharedPtr<Poco::Net::PrivateKeyPassphraseHandler> p_console_handler_;
		Poco::SharedPtr<Poco::Net::InvalidCertificateHandler> p_certificate_handler_;
		Poco::Net::Context::Ptr p_context_;

		std::string PostMessage(Poco::JSON::Object::Ptr message);
		Poco::JSON::Object::Ptr FormatMessage(const Poco::Message& msg) const;
		Poco::JSON::Object::Ptr FormatException(const Poco::Message& msg);
	};
}


StackifyChannel.cpp

#include "stdafx.h"
#include "StackifyChannel.h"

#include "Poco/Mutex.h"
#include "Poco/Timestamp.h"
#include "Poco/URI.h"
#include "Poco/Net/HTTPSClientSession.h"
#include "Poco/Net/HTTPRequest.h"
#include "Poco/Net/HTTPResponse.h"
#include "Poco/JSON/Object.h"
#include "Poco/JSON/Parser.h"
#include "Poco/Net/SSLManager.h"
#include "Poco/Net/SSLException.h"
#include "Poco/Net/AcceptCertificateHandler.h"
#include "Poco/Net/KeyConsoleHandler.h"

#include <sstream>

namespace DefenseAgainstTheDarkArts {

	const std::string StackifyChannel::PROP_APIKEY = "apiKey";
	const std::string StackifyChannel::PROP_ENVIRONMENT = "environment";
	const std::string StackifyChannel::PROP_SERVERNAME = "serverName";
	const std::string StackifyChannel::PROP_APPNAME = "appName";
	const std::string StackifyChannel::PROP_APPLOC = "appLocation";
	const std::string StackifyChannel::PROP_LOGGER = "logger";
	const std::string StackifyChannel::PROP_PLATFORM = "platform";
	const std::string StackifyChannel::PROP_OPERATINGSYSTEM = "operatingSystem";
	const std::string StackifyChannel::PROP_OSMAJORVERSION = "osMajorVersion";
	const std::string StackifyChannel::PROP_OSMINORVERSION = "osMinorVersion";
	const std::string StackifyChannel::PROP_TIMEZONE = "timezone";

	StackifyChannel::StackifyChannel() : str_(std::clog)
	{
		stackify_logging_save_log_url.append("https://api.stackify.com/log/save");

		logger_ = "stackify-log-pococpp1.0.0.0";
		platform_ = "CPP";

		Poco::Net::initializeSSL();
		p_console_handler_ = new Poco::Net::KeyConsoleHandler(false); //This is kind of silly, but required for proper initialization (otherwise warning - prompts for password when using client side ssl).
		p_certificate_handler_ = new Poco::Net::AcceptCertificateHandler(true);
		p_context_ = new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, "", "", "", Poco::Net::Context::VERIFY_NONE, 9, true, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); //Todo: Review (Verify NONE is not doing any checking.  Ok for now) need cert details

		Poco::Net::SSLManager::instance().initializeClient(p_console_handler_, p_certificate_handler_, p_context_);
	}

	StackifyChannel::StackifyChannel(std::ostream& str) : str_(str)
	{
		stackify_logging_save_log_url.append("https://api.stackify.com/log/save");

		logger_ = "stackify-log-pococpp1.0.0.0";
		platform_ = "CPP";

		Poco::Net::initializeSSL();
		p_console_handler_ = new Poco::Net::KeyConsoleHandler(false); //This is kind of silly, but required for proper initialization (otherwise warning - prompts for password when using client side ssl).
		p_certificate_handler_ = new Poco::Net::AcceptCertificateHandler(true);
		p_context_ = new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, "", "", "", Poco::Net::Context::VERIFY_NONE, 9, true, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); //Todo: Review (Verify NONE is not doing any checking.  Ok for now) need cert details

		Poco::Net::SSLManager::instance().initializeClient(p_console_handler_, p_certificate_handler_, p_context_);
	}

	StackifyChannel::~StackifyChannel()
	{
		Poco::Net::uninitializeSSL();
	}

	void StackifyChannel::log(const Poco::Message& msg)
	{
		Poco::FastMutex::ScopedLock lock(mutex_);

		try
		{
			const auto priority = msg.getPriority();
			if (priority == Poco::Message::PRIO_FATAL)
			{
				auto result = PostMessage(FormatException(msg));
			}
			else if (priority == Poco::Message::PRIO_CRITICAL)
			{
				auto result = PostMessage(FormatException(msg));
			}
			else if (priority == Poco::Message::PRIO_ERROR)
			{
				auto result = PostMessage(FormatException(msg));
			}
			else if (priority == Poco::Message::PRIO_WARNING)
			{
				auto result = PostMessage(FormatMessage(msg));
			}
			else if (priority == Poco::Message::PRIO_NOTICE)
			{
				auto result = PostMessage(FormatMessage(msg));
			}
			else if (priority == Poco::Message::PRIO_INFORMATION)
			{
				auto result = PostMessage(FormatMessage(msg));
			}
			else if (priority == Poco::Message::PRIO_DEBUG)
			{
				auto result = PostMessage(FormatMessage(msg));
			}
			else if (priority == Poco::Message::PRIO_TRACE)
			{
				auto result = PostMessage(FormatMessage(msg));
			}
		}
		catch(...)
		{
			//Swallow everything.  Remote logging exceptions cannot break the application.
		}
	}

	void StackifyChannel::setProperty(const std::string& name, const std::string& value)
	{
		Poco::FastMutex::ScopedLock lock(mutex_);

		if (name == PROP_APIKEY)
			api_key_ = value;
		else if (name == PROP_APPLOC)
			location_ = value;
		else if (name == PROP_APPNAME)
			app_name_ = value;
		else if (name == PROP_ENVIRONMENT)
			environment_ = value;
		else if (name == PROP_LOGGER)
			logger_ = value;
		else if (name == PROP_PLATFORM)
			platform_ = value;
		else if (name == PROP_SERVERNAME)
			server_name_ = value;
		else if (name == PROP_OPERATINGSYSTEM)
			operating_system_ = value;
		else if (name == PROP_OSMAJORVERSION)
			os_major_version_ = value;
		else if (name == PROP_OSMINORVERSION)
			os_minor_version_ = value;
		else if (name == PROP_TIMEZONE)
			timezone_ = value;
		else
			Channel::setProperty(name, value);
	}

	std::string StackifyChannel::getProperty(const std::string& name) const
	{
		if (name == PROP_APIKEY)
			return api_key_;
		if (name == PROP_APPLOC)
			return location_;
		if (name == PROP_APPNAME)
			return app_name_;
		if (name == PROP_ENVIRONMENT)
			return environment_;
		if (name == PROP_LOGGER)
			return logger_;
		if (name == PROP_PLATFORM)
			return platform_;
		if (name == PROP_SERVERNAME)
			return server_name_;
		if (name == PROP_OPERATINGSYSTEM)
			return operating_system_;
		if (name == PROP_OSMAJORVERSION)
			return os_major_version_;
		if (name == PROP_OSMINORVERSION)
			return os_minor_version_;
		if (name == PROP_TIMEZONE)
			return timezone_;

		return Channel::getProperty(name);
	}

	// ReSharper disable once CppMemberFunctionMayBeConst
	std::string StackifyChannel::PostMessage(Poco::JSON::Object::Ptr message)
	{

		try
		{
			//Create the HTTPSClientSession & initialize the request
			Poco::URI uri(stackify_logging_save_log_url);
			Poco::Net::HTTPSClientSession session(uri.getHost(), uri.getPort());
			Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_POST, uri.getPath(), Poco::Net::HTTPMessage::HTTP_1_1);
			session.setProxy("localhost", 8888);  //Enable this to send to local fiddler proxy


			std::stringstream body_stream;
			message->stringify(body_stream); //Used to set content length
			request.setKeepAlive(true);
			request.setContentLength(body_stream.str().size());
			request.setContentType("application/json");
			request.add("Accept", "application/json");
			request.add("X-Stackify-PV", "V1");
			request.add("X-Stackify-Key", api_key_);

			std::ostream& request_stream = session.sendRequest(request);
			message->stringify(request_stream); //Write to request stream (Send)

			// Receive the response.
			Poco::Net::HTTPResponse response;
			std::istream& response_stream = session.receiveResponse(response);

			if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_NOT_FOUND)
				throw Poco::ApplicationException("The requested resource was not found.");

			//Parse JSON Response
			Poco::JSON::Parser parser;
			Poco::Dynamic::Var result = parser.parse(response_stream);

			if (_stricmp(response.getContentType().c_str(), "application/json; charset=utf-8") == 0)
			{
				if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_OK)
				{
					return result.toString();
				}

				throw Poco::ApplicationException(response.getReason(), response.getStatus());
			}

			const std::string error = "Unexpected content type returned from server.  Expected (application/json; charset=utf-8) and received " + response.getContentType();
			throw Poco::ApplicationException(error);
		}
		catch (Poco::JSON::JSONException e)
		{
			throw Poco::ApplicationException("Session::Post() - JSON Exception", e);
		}
		catch (Poco::Net::SSLException e)
		{
			throw Poco::ApplicationException("Session::Post() - SSL Exception", e);
		}
	}

	Poco::JSON::Object::Ptr StackifyChannel::FormatMessage(const Poco::Message& msg) const
	{
		//Build Json
		Poco::JSON::Object::Ptr root = new Poco::JSON::Object(true);

		//Set Environment
		root->set("Env", environment_);

		//Set Server Name
		root->set("ServerName", server_name_);

		//Set App Name
		root->set("AppName", app_name_);

		//Set App Loc
		root->set("AppLoc", location_);

		//Set Logger
		root->set("Logger", logger_);

		//Set Platform
		root->set("Platform", platform_);

		//Set Message Array.  We are only sending one message at a time, but array is required
		Poco::JSON::Array::Ptr messages = new Poco::JSON::Array();

		//Message
		Poco::JSON::Object::Ptr message = new Poco::JSON::Object();
		message->set("Msg", msg.getText());
		message->set("Th", msg.getThread());

		//Epoch
		Poco::Timestamp epoch_time = msg.getTime();
		const auto epoch_milliseconds = epoch_time.epochMicroseconds() / 1000;

		std::stringstream epoch;
		epoch << epoch_milliseconds;

		message->set("EpochMs", epoch.str());

		//Source Method
		if (msg.getSourceFile() != nullptr)
			message->set("SrcMethod", msg.getSourceFile());

		//Source Line
		if (msg.getSourceLine() != 0)
			message->set("SrcLine,", msg.getSourceLine());

		//Message Priority
		const auto priority = msg.getPriority();
		if (priority == Poco::Message::PRIO_FATAL)
		{
			message->set("Level", "fatal");
		}
		else if (priority == Poco::Message::PRIO_CRITICAL)
		{
			message->set("Level", "critical");
		}
		else if (priority == Poco::Message::PRIO_ERROR)
		{
			message->set("Level", "error");
		}
		else if (priority == Poco::Message::PRIO_WARNING)
		{
			message->set("Level", "warning");
		}
		else if (priority == Poco::Message::PRIO_NOTICE)
		{
			message->set("Level", "notice");
		}
		else if (priority == Poco::Message::PRIO_INFORMATION)
		{
			message->set("Level", "info");
		}
		else if (priority == Poco::Message::PRIO_DEBUG)
		{
			message->set("Level", "debug");
		}
		else if (priority == Poco::Message::PRIO_TRACE)
		{
			message->set("Level", "trace");
		}

		//Add message to messages array
		messages->set(0, message);

		//Add messages array to root
		root->set("Msgs", messages);

		//Uncomment to debug json
		//std::stringstream sv;
		//root->stringify(sv, 4);
		//std::string svs = sv.str();

		return root;
	}

	// ReSharper disable once CppMemberFunctionMayBeStatic
	// ReSharper disable once CppMemberFunctionMayBeConst
	Poco::JSON::Object::Ptr StackifyChannel::FormatException(const Poco::Message & msg)
	{
		//Build Json
		Poco::JSON::Object::Ptr root = new Poco::JSON::Object(true);

		//Set Environment
		root->set("Env", environment_);

		//Set Server Name
		root->set("ServerName", server_name_);

		//Set App Name
		root->set("AppName", app_name_);

		//Set App Loc
		root->set("AppLoc", location_);

		//Set Logger
		root->set("Logger", logger_);

		//Set Platform
		root->set("Platform", platform_);

		//Set Message Array.  We are only sending one message at a time, but array is required
		Poco::JSON::Array::Ptr messages = new Poco::JSON::Array();

		//Message
		Poco::JSON::Object::Ptr message = new Poco::JSON::Object();
		message->set("Msg", msg.getText());
		message->set("Th", msg.getThread());

		//Epoch
		Poco::Timestamp epoch_time = msg.getTime();
		const auto epoch_milliseconds = epoch_time.epochMicroseconds() / 1000;

		std::stringstream epoch;
		epoch << epoch_milliseconds;

		message->set("EpochMs", epoch.str());

		//Source Method
		if (msg.getSourceFile() != nullptr)
			message->set("SrcMethod", msg.getSourceFile());

		//Source Line
		if (msg.getSourceLine() != 0)
			message->set("SrcLine,", msg.getSourceLine());

		//Message Priority
		const auto priority = msg.getPriority();
		if (priority == Poco::Message::PRIO_FATAL)
		{
			message->set("Level", "fatal");
		}
		else if (priority == Poco::Message::PRIO_CRITICAL)
		{
			message->set("Level", "critical");
		}
		else if (priority == Poco::Message::PRIO_ERROR)
		{
			message->set("Level", "error");
		}
		else if (priority == Poco::Message::PRIO_WARNING)
		{
			message->set("Level", "warning");
		}
		else if (priority == Poco::Message::PRIO_NOTICE)
		{
			message->set("Level", "notice");
		}
		else if (priority == Poco::Message::PRIO_INFORMATION)
		{
			message->set("Level", "info");
		}
		else if (priority == Poco::Message::PRIO_DEBUG)
		{
			message->set("Level", "debug");
		}
		else if (priority == Poco::Message::PRIO_TRACE)
		{
			message->set("Level", "trace");
		}

		//Exception
		Poco::JSON::Object::Ptr ex = new Poco::JSON::Object();
		Poco::JSON::Object::Ptr environment_detail = new Poco::JSON::Object();
		environment_detail->set("DeviceName", server_name_);
		environment_detail->set("AppLocation", location_);
		environment_detail->set("ConfiguredAppName", app_name_);
		environment_detail->set("ConfiguredEnvironmentName", environment_);

		//Add Environment Detail to Exception Object
		ex->set("EnvironmentDetail", environment_detail);
		message->set("Ex", ex);

		//Server Variables
		Poco::JSON::Object::Ptr server_variables = new Poco::JSON::Object();
		server_variables->set("OperatingSystem", operating_system_);
		server_variables->set("OsMajorVersion", os_major_version_);
		server_variables->set("OsMinorVersion", os_minor_version_);
		server_variables->set("TimeZone", timezone_);

		//Add ServerVariables to Exception Object
		ex->set("ServerVariables", server_variables);

		//Set OccuredEpochMillis
		ex->set("OccurredEpochMillis", epoch.str());
		
		Poco::JSON::Object::Ptr error = new Poco::JSON::Object();
		error->set("Message", msg.get("error_message"));
		error->set("ErrorType", msg.get("error_type"));
		error->set("SourceMethod", msg.get("source_method"));

		//Parse and Add StackTrace Information
		Poco::JSON::Array::Ptr stack_trace = new Poco::JSON::Array();
		const std::string stack = msg.get("stack_trace");

		//Parse the Stack into a vector by line
		std::stringstream stack_stream;
		stack_stream << stack;

		std::vector<std::string> stack_elements;
		std::string line;
		while (std::getline(stack_stream, line)) {
			stack_elements.push_back(line);
		}

		int stack_depth_counter = 0;
		for (std::vector<std::string>::iterator it = stack_elements.begin(); it != stack_elements.end(); ++it) {
			it->erase(std::remove(it->begin(), it->end(), '('), it->end()); //Remove parenthesis
			it->erase(std::remove(it->begin(), it->end(), ')'), it->end()); //Remove parenthesis
			it->erase(std::remove(it->begin(), it->end(), '"'), it->end()); //Remove quotation
			it->erase(std::remove(it->begin(), it->end(), ','), it->end()); //Remove commas

			std::stringstream elements;
			elements << it->c_str();

			//Tokenize
			std::vector<std::string> tokens{ std::istream_iterator<std::string>{elements}, std::istream_iterator<std::string>{} };

			//Remove unnecessary tokens (in and list)
			tokens.erase(std::remove(tokens.begin(), tokens.end(), "in"), tokens.end());
			tokens.erase(std::remove(tokens.begin(), tokens.end(), "line"), tokens.end());

			//Set Stack Element Object
			Poco::JSON::Object::Ptr stack_object = new Poco::JSON::Object();
			stack_object->set("CodeFileName", tokens[1]);
			stack_object->set("LineNum", tokens[2]);
			stack_object->set("Method", tokens[0]);

			stack_trace->set(stack_depth_counter, stack_object);
			stack_depth_counter++;
		}

		//Add StackTrace Array to Error Object
		error->set("StackTrace", stack_trace);
		ex->set("Error", error);
		
		//Add message to messages array
		messages->set(0, message);

		//Add messages array to root
		root->set("Msgs", messages);

		//Uncomment to debug json
		std::stringstream sv;
		root->stringify(sv, 4);
		std::string svs = sv.str();

		return root;
	}
}