Stackify with the C++ REST SDK - Windows

Stackify with the C++ REST SDK - Windows

In my previous post I added a Stackify Channel to the POCO logging framework, but in this example I am just using the Stackify REST api with the C++ REST SDK (Casablanca).  No fancy frameworks and loggers to deal with.  The C++ REST SDK provides us with a nice and simple async logger that wont get in the way.  However, there are some complexities around the build system and setup that are worth noting.  Please see the associated blurb in this post for details.

Setup

First of all if you dont know what VCPKG is you should check it out. VCPKG installs all of the dependencies you need and wires up everything you need to compile and link.  Nuget for C++ seems to have died a horrible death, but VCPKG seems pretty solid and is gaining adoption.  I'm hoping I will have time to turn this library into a package soon so I can learn how to do it.

  • Install the C++ SDK using VCPKG - VCPKG
  • Install the C++ SDK using VCPKG C++ REST SDK

vcpkg install cpprestsdk cpprestsdk:x64-windows vcpkg install cpprestsdk cpprestsdk:x86-windows

  • Install boost StackTrace

Boost Stacktrace is used to dump a call stack and send to stackify. You can install this with VCPKG as well using the following command.

vcpkg install boost-stacktrace:x86-windows vcpkg install boost-stacktrace:x64-windows

  • Fix a bug in C++ REST SDK with VS2017 (Maybe)

This step is the worst part of this project.  There is a bug for Visual Studio 15.x versions that cause a compile error with int_type in Streams.h of the C++ REST SDK.  It is really quite sad that this issue is still open, but a fix is documented here Fix

Error C7510 'int_type': use of dependent type name must be prefixed with typename'

FROM: #ifndef _WIN32 // Required by GCC
TO: //#ifndef _WIN32 // Required by GCC & MSVC

Hopefully you wont need this much longer, but it is what it is.

Logging

Initialize the logger with your api key, the application name, and the logging level.  In Stackify this snippet below will log to your account on an application named Android in the Development environment

DefenseAgainstTheDarkArts::Stackify stackify("Your api key here", "Sample", "Development");

Log something.  This library uses the same pattern as the other loggers.  You can log with the helper methods like Information, Warning, etc... or you can populate and pass in a message object yourself.
stackify.Information("This is a test information message");

You can also log exceptions or errors with this library.  Exceptions or errors include stack traces and more metadata about the system.

stackify_logger.Error(L"Some Error", L"Something is null and it shouldnt be", L"NULL Reference Exception", L"main", 222);

Stackify.h
#pragma once
#include <string>
#include "Priority.h"
#include "Exception.h"
#include "Message.h"
#include <cpprest/http_client.h> 
#include <ppltasks.h>
#include <cpprest/json.h>
#include <boost/stacktrace.hpp>

//This is just here from a macro/naming conflict
#ifdef FormatMessage
#undef FormatMessage
#endif

#ifdef ERROR
#undef ERROR
#endif

namespace DefenseAgainstTheDarkArts
{
	class Stackify
	{
	public:
		Stackify(std::wstring api_key, std::wstring application_name, std::wstring environment);
		~Stackify();

		void Trace(std::wstring text);
		void Debug(std::wstring text);
		void Information(std::wstring text);
		void Notice(std::wstring text);
		void Warning(std::wstring text, std::wstring source_method, int source_line);
		void Error(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line);
		void Critical(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line);
		void Fatal(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line);

		//Todo: Add more overrides?
		void Log(Message& message);
		void Log(Exception& exception);

		//Environment
		void SetEnvironment(std::wstring env);
		std::wstring GetEnvironment();

		void SetApplicationName(std::wstring app_name);
		std::wstring GetApplicationName();

		void SetServerVariable(std::wstring key, std::wstring value);
		std::wstring GetServerVariable(std::wstring key);

		bool log_server_variables;
	private:
		web::json::value FormatMessage(Message msg);
		web::json::value FormatException(Exception ex);
		pplx::task<void> PostMessage(std::wstring api_key, web::json::value message);

		std::wstring GetLocalComputerName();
		std::wstring GetOperatingSystemVersion();
		std::string GetWindowsTimeZoneName();
		std::wstring GetWorkingDirectory();

		std::wstring api_key_;
	    std::wstring environment_;
		std::wstring server_name_;
		std::wstring application_name_;
		std::wstring location_;
		std::wstring logger_;
		std::wstring platform_;
		
		std::map<std::wstring, std::wstring> server_variables_;
		//pplx::task<void> PostMessage(web::json::object message);
		
	};
}

Stackify.cpp
#include "stdafx.h"
#include "Stackify.h"
#include <Windows.h> 
#include <VersionHelpers.h>
#include <string>
#include <iostream>
#include <winerror.h>
#include <chrono>

using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace std::chrono;


namespace DefenseAgainstTheDarkArts
{
	Stackify::Stackify(std::wstring api_key, std::wstring application_name, std::wstring environment)
	{
		api_key_ = api_key;
		environment_ = environment;
		application_name_ = application_name; 
		platform_ = L"cpp";
		logger_ = L"defense-against-the-dark-arts-stackify-logger-1.0.0.0";
		server_name_ = GetLocalComputerName();
		location_ = GetWorkingDirectory();
		
		//Sets whether server variables will be logged as data on every message.  Otherwise Server variables are only logged on exceptions
		//Server variables are kvp of misc data that you can set
		log_server_variables = false;
	}

	Stackify::~Stackify()
	{
	}

	void Stackify::Trace(std::wstring text)
	{
		Message msg;
		msg.text = text;
		msg.priority = TRACE;

		web::json::value json = FormatMessage(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Debug(std::wstring text)
	{
		Message msg;
		msg.text = text;
		msg.priority = DEBUG;

		web::json::value json = FormatMessage(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Information(std::wstring text)
	{
		Message msg;
		msg.text = text;
		msg.priority = INFORMATION;

		web::json::value json = FormatMessage(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Notice(std::wstring text)
	{
		Message msg;
		msg.text = text;
		msg.priority = NOTICE;

		web::json::value json = FormatMessage(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Warning(std::wstring text, std::wstring source_method, int source_line)
	{
		Message msg;
		msg.text = text;
		msg.priority = WARNING;
		msg.source_method = source_method;
		msg.source_line = source_line;

		web::json::value json = FormatMessage(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Error(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line)
	{
		Exception msg;
		msg.text = text;
		msg.priority = ERROR;
		msg.source_method = source_method;
		msg.source_line = source_line;
		msg.error_message = error_message;
		msg.error_type = error_type;

		web::json::value json = FormatException(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Critical(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line)
	{
		Exception msg;
		msg.text = text;
		msg.priority = CRITICAL;
		msg.source_method = source_method;
		msg.source_line = source_line;
		msg.error_message = error_message;
		msg.error_type = error_type;

		web::json::value json = FormatException(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Fatal(std::wstring text, std::wstring error_message, std::wstring error_type, std::wstring source_method, int source_line)
	{
		Exception msg;
		msg.text = text;
		msg.priority = FATAL;
		msg.source_method = source_method;
		msg.source_line = source_line;
		msg.error_message = error_message;
		msg.error_type = error_type;

		web::json::value json = FormatException(msg);
		PostMessage(api_key_, json);
	}

	void Stackify::Log(Message& message)
	{
		web::json::value json = FormatMessage(message);
		PostMessage(api_key_, json);
	}

	void Stackify::Log(Exception& exception)
	{
		web::json::value json = FormatException(exception);
		PostMessage(api_key_, json);
	}

	web::json::value Stackify::FormatMessage(Message msg)
	{
		json::value root;
		root[L"Env"] = json::value::string(environment_);
		root[L"ServerName"] = json::value::string(server_name_);
		root[L"AppName"] = json::value::string(application_name_);
		root[L"AppLoc"] = json::value::string(location_);
		root[L"Logger"] = json::value::string(logger_);
		root[L"Platform"] = json::value::string(platform_);

		// Create a JSON message
		json::value message;
		message[L"Msg"] = json::value::string(msg.text);
		message[L"Th"] = json::value::string(msg.thread_name);

		switch (msg.priority) {
		case TRACE:
			message[L"Level"] = json::value::string(L"Trace");
			break;
		case DEBUG:
			message[L"Level"] = json::value::string(L"Debug");
			break;
		case INFORMATION:
			message[L"Level"] = json::value::string(L"Information");
			break;
		case NOTICE:
			message[L"Level"] = json::value::string(L"Notice");
			break;
		case WARNING:
			message[L"Level"] = json::value::string(L"Warning");
			break;
		case ERROR:
			message[L"Level"] = json::value::string(L"Error");
			break;
		case CRITICAL:
			message[L"Level"] = json::value::string(L"Critical");
			break;
		case FATAL:
			message[L"Level"] = json::value::string(L"Fatal");
			break;
		}
		
		//Get Epoch Milliseconds
		milliseconds ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
		message[L"EpochMs"] = json::value::number(ms.count());

		message[L"SrcMethod"] = json::value::string(msg.source_method);
		message[L"SrcLine"] = json::value::number(msg.source_line);

		//Populate Data Values
		std::wstringstream data;
		for (auto it = msg.data_map.begin(); it != msg.data_map.end(); ++it)
		{
			//Example: Long string containing "Key1":"Value1", "Key2" : "Value2"
			//Todo: Escape special characters
			data << it->first;
			data << ":";
			data << it->second;

			//Check  if it is the last element otherwise add a comma
			if (++it != msg.data_map.end())
			{
				data << ",";
			}
			else
			{
				//If it is the last element check to see if server variables should be appended to the message as data
				if (log_server_variables)
				{
					if (!server_variables_.size() > 0)
					{
						//Has values so add a comma
						data << ",";
					}
				}
			}
		}

		//Append server variables to data
		if (log_server_variables)
		{
			for (auto it = server_variables_.begin(); it != server_variables_.end(); ++it)
			{
				//Nothing in data so append as normal
				data << it->first;
				data << ":";
				data << it->second;

				//Check  if it is the last element
				if (++it != server_variables_.end())
				{
					data << ",";
				}
			}
		}

		if (!data.str().empty())
		{
			message[L"Data"] = json::value::string(data.str());
		}
		
		// Create the messages array
		json::value messages;
		messages[0] = message;

		// Assign the items array as the value for the Messages key
		root[L"Msgs"] = messages;

		return root;
	}

	web::json::value Stackify::FormatException(Exception ex)
	{
		json::value root;
		root[L"Env"] = json::value::string(environment_);
		root[L"ServerName"] = json::value::string(server_name_);
		root[L"AppName"] = json::value::string(application_name_);
		root[L"AppLoc"] = json::value::string(location_);
		root[L"Logger"] = json::value::string(U("defense-against-the-dark-arts-stackify-logger-pococpp-1.0.0.0"));
		root[L"Platform"] = json::value::string(U("cpp"));

		// Create a JSON message
		json::value message;
		message[L"Msg"] = json::value::string(ex.text);
		message[L"Th"] = json::value::string(ex.thread_name);

		switch (ex.priority) {
		case TRACE:
			message[L"Level"] = json::value::string(L"Trace");
			break;
		case DEBUG:
			message[L"Level"] = json::value::string(L"Debug");
			break;
		case INFORMATION:
			message[L"Level"] = json::value::string(L"Information");
			break;
		case NOTICE:
			message[L"Level"] = json::value::string(L"Notice");
			break;
		case WARNING:
			message[L"Level"] = json::value::string(L"Warning");
			break;
		case ERROR:
			message[L"Level"] = json::value::string(L"Error");
			break;
		case CRITICAL:
			message[L"Level"] = json::value::string(L"Critical");
			break;
		case FATAL:
			message[L"Level"] = json::value::string(L"Fatal");
			break;
		}

		//Get Epoch Milliseconds
		milliseconds ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch());
		message[L"EpochMs"] = json::value::number(ms.count());

		message[L"SrcMethod"] = json::value::string(ex.source_method);
		message[L"SrcLine"] = json::value::number(ex.source_line);

		//Environment Detail
		json::value environment_detail;
		environment_detail[L"DeviceName"] = json::value::string(server_name_);
		environment_detail[L"AppLocation"] = json::value::string(location_);
		environment_detail[L"ConfiguredAppName"] = json::value::string(application_name_);
		environment_detail[L"ConfiguredEnvironmentName"] = json::value::string(environment_);

		//Server Variables
		if (server_variables_.size() > 0)
		{
			json::value server_variables;
			for (auto it = server_variables_.begin(); it != server_variables_.end(); ++it)
			{
				server_variables[it->first] = json::value::string(it->second);
			}

			message[L"ServerVariables"] = server_variables;
		}
		
		//Error object
		json::value error;
		error[L"Message"] = json::value::string(ex.error_message);
		error[L"ErrorType"] = json::value::string(ex.error_type);
		error[L"ErrorTypeCode"] = json::value::string(ex.error_type_code);
		error[L"SourceMethod"] = json::value::string(ex.source_method);
		error[L"CustomerName"] = json::value::string(ex.customer_name);
		error[L"UserName"] = json::value::string(ex.user_name);

		//Error Data Object
		if (ex.error_data_map.size() > 0)
		{
			json::value error_data;
			for (auto it = ex.error_data_map.begin(); it != ex.error_data_map.end(); ++it)
			{
				error_data[it->first] = json::value::string(it->second);
			}

			error[L"Data"] = error_data;
		}
		
		//StackTrace
		//Todo: Review - Should caller pass in stack trace or just generate?  Leaning toward generate for simplicity
		json::value stack_frames;
		int frame_count = 0;

		boost::stacktrace::basic_stacktrace<> stack_trace = boost::stacktrace::stacktrace();
		for (auto it = stack_trace.begin(); it != stack_trace.end(); ++it)
		{
			//Todo: Remove the first 4 sections of stack trace.  Shows this method
			//Convert to source_file to wstring
			std::string source_file = it->source_file();
			std::wstring ws_source_file;
			ws_source_file.assign(source_file.begin(), source_file.end());

			//Convert source line to wstring
			std::wstringstream source_line;
			source_line << it->source_line();

			//Convert method to wstring
			std::string method = it->name();
			std::wstring ws_method;
			ws_method.assign(method.begin(), method.end());
			
			json::value stack_frame;
			stack_frame[L"CodeFileName"] = json::value::string(ws_source_file);	
			stack_frame[L"LineNum"] = json::value::string(source_line.str());
			stack_frame[L"Method"] = json::value::string(ws_method);
			
			stack_frames[frame_count] = stack_frame;
			frame_count++;
		}

		error[L"StackTrace"] = stack_frames;

		//Add Environment Detail to Exception object
		json::value stackify_exception;
		stackify_exception[L"EnvironmentDetail"] = environment_detail;
		stackify_exception[L"OccurredEpochMillis"] = json::value::number(ms.count());

		stackify_exception[L"Error"] = error;

		//Add Exception detail to message object
		message[L"Ex"] = stackify_exception;

		//Todo:: Add web request details
		
		// Create the messages array
		json::value messages;
		messages[0] = message;

		// Assign the items array as the value for the Messages key
		root[L"Msgs"] = messages;

		return root;
	}

	std::wstring Stackify::GetLocalComputerName()
	{
		TCHAR computer_name[MAX_COMPUTERNAME_LENGTH + 1];
		DWORD size = sizeof(computer_name);
		GetComputerNameW(computer_name, &size);

		return computer_name;
	}

	//I dont like this over GetWindowsVersionEx, but that is deprecated
	//Todo: Look for more detailed version information api (Registry?)
	std::wstring Stackify::GetOperatingSystemVersion()
	{
		if (IsWindows10OrGreater())
		{
			return L"Windows 10";
		}

		if (IsWindows8Point1OrGreater())
		{
			return L"Windows 8.1";
		}

		if (IsWindows8OrGreater())
		{
			return L"Windows 8";
		}

		if (IsWindows7SP1OrGreater())
		{
			return L"Windows 7 with Service Pack 1";
		}

		if (IsWindows7OrGreater())
		{
			return L"Windows 7";
		}

		if (IsWindowsVistaSP2OrGreater())
		{
			return L"Windows Vista with Service Pack 2";
		}

		if (IsWindowsVistaSP1OrGreater())
		{
			return L"Windows Vista with Service Pack 1";
		}

		if (IsWindowsVistaOrGreater())
		{
			return L"Windows Vista";
		}

		if (IsWindowsXPSP3OrGreater())
		{
			return L"Windows XP with Service Pack 3";
		}

		if (IsWindowsXPSP2OrGreater())
		{
			return L"Windows XP with Service Pack 2";
		}

		if (IsWindowsXPSP1OrGreater())
		{
			return L"Windows XP with Service Pack 1";
		}

		if (IsWindowsXPOrGreater())
		{
			return L"Windows XP";
		}

		if (IsWindowsServer())
		{
			return L"Windows Server";
		}

		return L"Unknown";
	}

	std::string Stackify::GetWindowsTimeZoneName()
	{
		const char *subkey = "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation";
		constexpr size_t keysize{ 128 };
		HKEY key;
		char key_name[keysize]{};
		unsigned long tz_keysize = keysize;
		if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, subkey, 0,
			KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
		{
			if (RegQueryValueExA(key, "TimeZoneKeyName", nullptr, nullptr,
				reinterpret_cast<LPBYTE>(key_name), &tz_keysize) != ERROR_SUCCESS)
			{
				memset(key_name, 0, tz_keysize);
			}
			RegCloseKey(key);
		}

		return std::string(key_name);
	}

	std::wstring Stackify::GetWorkingDirectory()
	{
		TCHAR result[MAX_PATH];
		return std::wstring(result, GetModuleFileNameW(nullptr, result, MAX_PATH));
	}

	pplx::task<void> Stackify::PostMessage(std::wstring api_key, json::value message)
	{
		// Manually build up an HTTP request with header and request URI.
		http_request request(methods::POST);

		request.headers().add(L"Content-Type", L"application/json");
		request.headers().add(L"Accept", L"application/json");
		request.headers().add(L"X-Stackify-PV", L"V1");
		request.headers().add(L"X-Stackify-Key", api_key.c_str());
		
		request.set_request_uri(L"log/save");
		request.set_body(message);

		http_client client(L"https://api.stackify.com");

		return client.request(request).then([](web::http::http_response response)
		{
			//This is really just fire and forget.  Uncomment for debugging
			/*if (response.status_code() == status_codes::OK)
			{
				auto body = response.extract_string();
				std::wcout << L"Updated: " << body.get().c_str() << std::endl;
			}*/
		});
	}

	void Stackify::SetEnvironment(std::wstring env)
	{
		environment_ = env;
	}

	std::wstring Stackify::GetEnvironment()
	{
		return environment_;
	}

	void Stackify::SetApplicationName(std::wstring app_name)
	{
		application_name_ = app_name;
	}

	std::wstring Stackify::GetApplicationName()
	{
		return application_name_;
	}

	void Stackify::SetServerVariable(std::wstring key, std::wstring value)
	{
		server_variables_.insert_or_assign(key, value);
	}

	std::wstring Stackify::GetServerVariable(std::wstring key)
	{
		const std::map<std::wstring, std::wstring>::iterator it = server_variables_.find(key);
		if (it != server_variables_.end())
			return it->second;

		return L"Key Not Found";
	}
}