Skip to main content

C++ Integration Guide

Overview

This guide explains how to integrate the AmpliServ Backend SDK with your C++ application. Since C++ doesn't have built-in logging libraries like other frameworks, we'll use spdlog for structured JSON logging and the AmpliServ SDK to collect the logs.

Prerequisites

  • C++17 or higher
  • CMake 3.14+
  • spdlog library (for JSON logging)
  • nlohmann/json library (for JSON handling)
  • AmpliServ SDK binary

Step 1: Install Dependencies

# Install vcpkg
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh

# Install spdlog and nlohmann-json
./vcpkg install spdlog
./vcpkg install nlohmann-json

Using Conan

# Install dependencies via Conan
conan install . --build=missing

Manual Installation (Linux)

# Install spdlog
sudo apt-get install libspdlog-dev

# Install nlohmann-json
sudo apt-get install nlohmann-json3-dev

Step 2: Create Project Structure

cpp-erp-backend/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── logger.cpp
│ ├── logger.hpp
│ ├── correlation_id.cpp
│ ├── correlation_id.hpp
│ ├── test_controller.cpp
│ ├── test_controller.hpp
│ ├── exception_handler.cpp
│ └── exception_handler.hpp
├── include/
│ └── utils.hpp
├── build/
└── app.log (created at runtime)

Step 3: CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(CppErpBackend VERSION 1.0.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find required packages
find_package(spdlog REQUIRED)
find_package(nlohmann_json REQUIRED)

# For UUID generation (Linux)
find_package(PkgConfig REQUIRED)
pkg_check_modules(UUID REQUIRED uuid)

# For threading
find_package(Threads REQUIRED)

# Include directories
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
include_directories(${spdlog_INCLUDE_DIRS})
include_directories(${nlohmann_json_INCLUDE_DIRS})

# Source files
set(SOURCES
src/main.cpp
src/logger.cpp
src/correlation_id.cpp
src/test_controller.cpp
src/exception_handler.cpp
)

# Executable
add_executable(cpp-erp-backend ${SOURCES})

# Link libraries
target_link_libraries(cpp-erp-backend
spdlog::spdlog
nlohmann_json::nlohmann_json
Threads::Threads
${UUID_LIBRARIES}
)

# For Windows, add bcrypt for UUID
if(WIN32)
target_link_libraries(cpp-erp-backend bcrypt)
endif()

Step 4: Logger Implementation

src/logger.hpp

#pragma once

#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/pattern_formatter.h>
#include <nlohmann/json.hpp>
#include <memory>
#include <string>

using json = nlohmann::json;

class Logger {
public:
static Logger& getInstance();

void initialize(const std::string& serviceName,
const std::string& environment,
const std::string& logFilePath = "app.log");

void info(const std::string& message, const json& extra = json::object());
void warn(const std::string& message, const json& extra = json::object());
void error(const std::string& message, const json& extra = json::object());
void debug(const std::string& message, const json& extra = json::object());

void errorWithException(const std::string& message,
const std::exception& ex,
const json& extra = json::object());

void setCorrelationId(const std::string& correlationId);
std::string getCorrelationId() const;

private:
Logger() = default;
~Logger() = default;

void log(spdlog::level::level_enum level,
const std::string& message,
const json& extra);

std::shared_ptr<spdlog::logger> m_logger;
std::string m_correlationId;
std::string m_serviceName;
std::string m_environment;

static std::unique_ptr<Logger> s_instance;
static std::once_flag s_onceFlag;
};

src/logger.cpp

#include "logger.hpp"
#include <chrono>
#include <iomanip>
#include <sstream>
#include <thread>

std::unique_ptr<Logger> Logger::s_instance = nullptr;
std::once_flag Logger::s_onceFlag;

Logger& Logger::getInstance() {
std::call_once(s_onceFlag, []() {
s_instance.reset(new Logger());
});
return *s_instance;
}

void Logger::initialize(const std::string& serviceName,
const std::string& environment,
const std::string& logFilePath) {
m_serviceName = serviceName;
m_environment = environment;

try {
// Create file sink for JSON logs
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
logFilePath, true
);

// Create console sink for debugging
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

// Create logger with multiple sinks
m_logger = std::make_shared<spdlog::logger>(
"json_logger",
spdlog::sinks_init_list{file_sink, console_sink}
);

// Set custom formatter for JSON output
m_logger->set_pattern("%v"); // Just the message (already JSON)

m_logger->set_level(spdlog::level::info);

spdlog::register_logger(m_logger);

info("ERP Backend started", {
{"port", 3000},
{"service", m_serviceName},
{"environment", m_environment}
});

} catch (const spdlog::spdlog_ex& ex) {
std::cerr << "Log initialization failed: " << ex.what() << std::endl;
}
}

void Logger::setCorrelationId(const std::string& correlationId) {
m_correlationId = correlationId;
}

std::string Logger::getCorrelationId() const {
return m_correlationId.empty() ? "unknown" : m_correlationId;
}

void Logger::info(const std::string& message, const json& extra) {
log(spdlog::level::info, message, extra);
}

void Logger::warn(const std::string& message, const json& extra) {
log(spdlog::level::warn, message, extra);
}

void Logger::error(const std::string& message, const json& extra) {
log(spdlog::level::err, message, extra);
}

void Logger::debug(const std::string& message, const json& extra) {
log(spdlog::level::debug, message, extra);
}

void Logger::errorWithException(const std::string& message,
const std::exception& ex,
const json& extra) {
json logEntry = extra;
logEntry["message"] = message;
logEntry["error"] = ex.what();
logEntry["stack_trace"] = ex.what(); // In production, use proper stack trace library

log(spdlog::level::err, message, logEntry);
}

void Logger::log(spdlog::level::level_enum level,
const std::string& message,
const json& extra) {
auto now = std::chrono::system_clock::now();
auto now_time_t = std::chrono::system_clock::to_time_t(now);
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
) % 1000;

std::stringstream timestamp;
timestamp << std::put_time(std::localtime(&now_time_t), "%Y-%m-%dT%H:%M:%S");
timestamp << "." << std::setfill('0') << std::setw(3) << now_ms.count();
timestamp << "Z";

json logEntry;
logEntry["timestamp"] = timestamp.str();
logEntry["level"] = spdlog::level::to_string_view(level);
logEntry["message"] = message;
logEntry["correlation_id"] = m_correlationId;
logEntry["service"] = m_serviceName;
logEntry["environment"] = m_environment;

// Merge extra fields
for (auto& [key, value] : extra.items()) {
logEntry[key] = value;
}

std::string jsonStr = logEntry.dump();

switch (level) {
case spdlog::level::info:
m_logger->info(jsonStr);
break;
case spdlog::level::warn:
m_logger->warn(jsonStr);
break;
case spdlog::level::err:
m_logger->error(jsonStr);
break;
case spdlog::level::debug:
m_logger->debug(jsonStr);
break;
default:
m_logger->info(jsonStr);
break;
}
}

Step 5: Correlation ID Implementation

src/correlation_id.hpp

#pragma once

#include <string>
#include <thread>
#include <unordered_map>

class CorrelationIdManager {
public:
static CorrelationIdManager& getInstance();

std::string generateCorrelationId();
std::string getCurrentCorrelationId() const;
void setCurrentCorrelationId(const std::string& correlationId);
void clearCurrentCorrelationId();

private:
CorrelationIdManager() = default;

static thread_local std::string m_currentCorrelationId;
static std::unique_ptr<CorrelationIdManager> s_instance;
static std::once_flag s_onceFlag;
};

src/correlation_id.cpp

#include "correlation_id.hpp"
#include <random>
#include <sstream>
#include <iomanip>
#include <chrono>

#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#else
#include <uuid/uuid.h>
#endif

thread_local std::string CorrelationIdManager::m_currentCorrelationId;
std::unique_ptr<CorrelationIdManager> CorrelationIdManager::s_instance = nullptr;
std::once_flag CorrelationIdManager::s_onceFlag;

CorrelationIdManager& CorrelationIdManager::getInstance() {
std::call_once(s_onceFlag, []() {
s_instance.reset(new CorrelationIdManager());
});
return *s_instance;
}

std::string CorrelationIdManager::generateCorrelationId() {
#ifdef _WIN32
UUID uuid;
UuidCreate(&uuid);

RPC_CSTR str;
UuidToStringA(&uuid, &str);

std::string result(reinterpret_cast<char*>(str));
RpcStringFreeA(&str);

return result;
#else
uuid_t uuid;
uuid_generate(uuid);

char str[37];
uuid_unparse(uuid, str);

return std::string(str);
#endif
}

std::string CorrelationIdManager::getCurrentCorrelationId() const {
if (m_currentCorrelationId.empty()) {
return "unknown";
}
return m_currentCorrelationId;
}

void CorrelationIdManager::setCurrentCorrelationId(const std::string& correlationId) {
m_currentCorrelationId = correlationId;
Logger::getInstance().setCorrelationId(correlationId);
}

void CorrelationIdManager::clearCurrentCorrelationId() {
m_currentCorrelationId.clear();
}

Step 6: Exception Handler

src/exception_handler.hpp

#pragma once

#include <exception>
#include <string>
#include <functional>

class ExceptionHandler {
public:
static ExceptionHandler& getInstance();

using ExceptionCallback = std::function<void(const std::exception&, const std::string&)>;

void setCallback(ExceptionCallback callback);
void handleException(const std::exception& ex, const std::string& context = "");

private:
ExceptionHandler() = default;
ExceptionCallback m_callback;
};

src/exception_handler.cpp

#include "exception_handler.hpp"
#include "logger.hpp"

ExceptionHandler& ExceptionHandler::getInstance() {
static ExceptionHandler instance;
return instance;
}

void ExceptionHandler::setCallback(ExceptionCallback callback) {
m_callback = callback;
}

void ExceptionHandler::handleException(const std::exception& ex, const std::string& context) {
auto& logger = Logger::getInstance();

json extra;
extra["context"] = context;
extra["stack_trace"] = ex.what(); // Full stack trace captured here

logger.errorWithException(ex.what(), ex, extra);

if (m_callback) {
m_callback(ex, context);
}
}

Step 7: Test Controller

src/test_controller.hpp

#pragma once

#include <string>
#include <functional>

class TestController {
public:
using HttpResponse = std::function<void(int, const std::string&)>;

TestController();

void handleInfo(HttpResponse callback);
void handleWarn(HttpResponse callback);
void handleError(HttpResponse callback);
void handleDashboard(HttpResponse callback);

private:
void simulateDeepError();
void level1();
void level2();
void level3();

std::string m_correlationId;
};

src/test_controller.cpp

#include "test_controller.hpp"
#include "logger.hpp"
#include "correlation_id.hpp"
#include "exception_handler.hpp"
#include <nlohmann/json.hpp>

TestController::TestController() = default;

void TestController::handleInfo(HttpResponse callback) {
auto& logger = Logger::getInstance();
auto& corrMgr = CorrelationIdManager::getInstance();

std::string correlationId = corrMgr.getCurrentCorrelationId();

logger.info("Info endpoint called - This is a normal log", {
{"correlation_id", correlationId}
});

json response;
response["message"] = "Info logged successfully";
response["correlationId"] = correlationId;

callback(200, response.dump());
}

void TestController::handleWarn(HttpResponse callback) {
auto& logger = Logger::getInstance();
auto& corrMgr = CorrelationIdManager::getInstance();

std::string correlationId = corrMgr.getCurrentCorrelationId();

logger.warn("Warning endpoint called - This is a warning log", {
{"correlation_id", correlationId}
});

json response;
response["message"] = "Warning logged";
response["correlationId"] = correlationId;

callback(200, response.dump());
}

void TestController::handleError(HttpResponse callback) {
auto& logger = Logger::getInstance();
auto& corrMgr = CorrelationIdManager::getInstance();
auto& exHandler = ExceptionHandler::getInstance();

std::string correlationId = corrMgr.getCurrentCorrelationId();

logger.info("Test error endpoint called", {
{"correlation_id", correlationId}
});

try {
simulateDeepError();

json response;
response["message"] = "No error";
response["correlationId"] = correlationId;

callback(200, response.dump());

} catch (const std::exception& ex) {
// Full stack trace captured here
exHandler.handleException(ex, "Test error endpoint");

json response;
response["message"] = "Error triggered";
response["correlationId"] = correlationId;
response["error"] = ex.what();

callback(500, response.dump());
}
}

void TestController::handleDashboard(HttpResponse callback) {
auto& logger = Logger::getInstance();
auto& corrMgr = CorrelationIdManager::getInstance();

std::string correlationId = corrMgr.getCurrentCorrelationId();

logger.info("Dashboard endpoint called", {
{"correlation_id", correlationId}
});

json dashboard;
dashboard["totalProducts"] = 100;
dashboard["totalOrders"] = 50;
dashboard["totalCustomers"] = 75;
dashboard["totalRevenue"] = 250000;
dashboard["correlationId"] = correlationId;

logger.debug("Dashboard data", dashboard);

callback(200, dashboard.dump());
}

void TestController::simulateDeepError() {
level1();
}

void TestController::level1() {
level2();
}

void TestController::level2() {
level3();
}

void TestController::level3() {
throw std::runtime_error("This is a test error with deep stack trace from C++!");
}

Step 8: Main Application

src/main.cpp

#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <cstdlib>

#include "logger.hpp"
#include "correlation_id.hpp"
#include "test_controller.hpp"
#include "exception_handler.hpp"

// Simple HTTP server simulation
// In production, use a proper HTTP library like Crow, Pistache, or Beast

void handleRequest(const std::string& method,
const std::string& path,
const std::string& correlationId) {

auto& corrMgr = CorrelationIdManager::getInstance();
auto& logger = Logger::getInstance();

// Set correlation ID for this request
corrMgr.setCurrentCorrelationId(correlationId);

// Log incoming request
logger.info("Incoming " + method + " " + path, {
{"method", method},
{"path", path},
{"correlation_id", correlationId}
});

auto startTime = std::chrono::steady_clock::now();

TestController controller;
int statusCode = 200;
std::string responseBody;

// Route handling
if (method == "GET") {
if (path == "/api/test/info") {
controller.handleInfo([&](int code, const std::string& body) {
statusCode = code;
responseBody = body;
});
}
else if (path == "/api/test/warn") {
controller.handleWarn([&](int code, const std::string& body) {
statusCode = code;
responseBody = body;
});
}
else if (path == "/api/test/error") {
controller.handleError([&](int code, const std::string& body) {
statusCode = code;
responseBody = body;
});
}
else if (path == "/api/dashboard") {
controller.handleDashboard([&](int code, const std::string& body) {
statusCode = code;
responseBody = body;
});
}
else {
statusCode = 404;
responseBody = R"({"message":"Not found"})";
logger.warn("Route not found: " + path, {{"correlation_id", correlationId}});
}
}

auto endTime = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();

// Log response
auto logLevel = statusCode >= 400 ? "error" : "info";
logger.info(method + " " + path + " " + std::to_string(statusCode) + " - " + std::to_string(duration) + "ms", {
{"method", method},
{"path", path},
{"statusCode", statusCode},
{"duration", duration},
{"correlation_id", correlationId}
});

// Print response to console (simulating HTTP response)
std::cout << "\n=== Response ===" << std::endl;
std::cout << "Status: " << statusCode << std::endl;
std::cout << "Body: " << responseBody << std::endl;
std::cout << "===============\n" << std::endl;

// Clear correlation ID for next request
corrMgr.clearCurrentCorrelationId();
}

int main(int argc, char* argv[]) {
auto& logger = Logger::getInstance();
auto& corrMgr = CorrelationIdManager::getInstance();
auto& exHandler = ExceptionHandler::getInstance();

// Set exception handler callback
exHandler.setCallback([](const std::exception& ex, const std::string& context) {
auto& logger = Logger::getInstance();
logger.error("Unhandled exception in " + context, {
{"error", ex.what()}
});
});

// Initialize logger
logger.initialize("cpp-erp-backend", "development", "app.log");

std::cout << "C++ ERP Backend running on http://localhost:3000" << std::endl;
std::cout << "Press Ctrl+C to exit" << std::endl;
std::cout << std::endl;

// Simple command-line interface for testing
while (true) {
std::cout << "\nAvailable commands:" << std::endl;
std::cout << " 1. Test Info" << std::endl;
std::cout << " 2. Test Warning" << std::endl;
std::cout << " 3. Test Error (with stack trace)" << std::endl;
std::cout << " 4. Dashboard" << std::endl;
std::cout << " 5. Exit" << std::endl;
std::cout << "Enter choice: ";

int choice;
std::cin >> choice;

if (choice == 5) {
std::cout << "Shutting down..." << std::endl;
break;
}

// Generate correlation ID for this request
std::string correlationId = corrMgr.generateCorrelationId();

std::string method = "GET";
std::string path;

switch (choice) {
case 1:
path = "/api/test/info";
break;
case 2:
path = "/api/test/warn";
break;
case 3:
path = "/api/test/error";
break;
case 4:
path = "/api/dashboard";
break;
default:
std::cout << "Invalid choice" << std::endl;
continue;
}

handleRequest(method, path, correlationId);
}

return 0;
}

Step 9: Configure AmpliServ SDK

Create run-agent.sh (Linux/Mac):

#!/bin/bash
export AMPLISERV_API_KEY="your-api-key-here"
export AMPLISERV_COLLECTOR_MODE="file"
export AMPLISERV_ENDPOINT="http://dev-backend.ampliserv.com/api/ingestion/v1/ingest"
export AMPLISERV_SERVICE_NAME="ingestion-service"
export AMPLISERV_ENV="development"
export AMPLISERV_LOG_PATH="/path/to/cpp-erp-backend/app.log"
export AMPLISERV_BATCH_SIZE="50"
export AMPLISERV_FLUSH_INTERVAL_SEC="5"
export AMPLISERV_INCIDENT_ENABLED="true"

echo "Starting AmpliServ Agent..."
./ampliserv-agent

Create run-agent.bat (Windows):

@echo off
set AMPLISERV_API_KEY=your-api-key-here
set AMPLISERV_COLLECTOR_MODE=file
set AMPLISERV_ENDPOINT=http://dev-backend.ampliserv.com/api/ingestion/v1/ingest
set AMPLISERV_SERVICE_NAME=ingestion-service
set AMPLISERV_ENV=development
set AMPLISERV_LOG_PATH=C:\path\to\cpp-erp-backend\app.log
set AMPLISERV_BATCH_SIZE=50
set AMPLISERV_FLUSH_INTERVAL_SEC=5
set AMPLISERV_INCIDENT_ENABLED=true

echo Starting AmpliServ Agent...
ampliserv-agent.exe

Step 10: Build and Run

Build with CMake:

mkdir build
cd build
cmake ..
make

Run the application:

./cpp-erp-backend

In a separate terminal, start the AmpliServ SDK:

./run-agent.sh

Step 11: Test the Integration

From the application menu:

Available commands:
1. Test Info
2. Test Warning
3. Test Error (with stack trace)
4. Dashboard
5. Exit
Enter choice: 1

Or use curl with a real HTTP server:

# If you implement a real HTTP server
curl http://localhost:3000/api/test/info
curl http://localhost:3000/api/test/error

Sample JSON Log Output

{
"timestamp": "2026-03-23T10:30:45.123Z",
"level": "error",
"message": "Test error endpoint called",
"correlation_id": "a1270278-7ef9-4e59-803e-e9862c257d73",
"service": "cpp-erp-backend",
"environment": "development",
"context": "Test error endpoint",
"stack_trace": "This is a test error with deep stack trace from C++!",
"error": "This is a test error with deep stack trace from C++!"
}

Production Recommendations

For Production, Use a Real HTTP Server:

Option 1: Crow (Header-only)

#include "crow.h"

int main() {
crow::SimpleApp app;

CROW_ROUTE(app, "/api/test/error")
([](const crow::request& req) {
auto& logger = Logger::getInstance();
// ... handler logic
return crow::response(200);
});

app.port(3000).run();
}

Option 2: Pistache

#include <pistache/endpoint.h>
#include <pistache/router.h>

class ErpHandler : public Http::Handler {
void onRequest(const Http::Request& req, Http::ResponseWriter response) override {
// ... handler logic
}
};

Docker Deployment

FROM ubuntu:22.04

RUN apt-get update && apt-get install -y \
libspdlog-dev \
nlohmann-json3-dev \
libuuid-dev

WORKDIR /app
COPY build/cpp-erp-backend .
COPY run-agent.sh .

CMD ["./cpp-erp-backend"]

Performance Considerations

Asynchronous Logging: spdlog supports async logging

auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_logger", "app.log");

Log Level in Production: Set to WARN or ERROR only

logger.set_level(spdlog::level::warn);

Batch Processing: The AmpliServ SDK handles batching, so your app just writes individual logs

Verification Checklist

  • C++ application compiles without errors
  • app.log file is created with JSON content
  • Logs are in valid JSON format
  • AmpliServ SDK shows "Starting AmpliServ Agent"
  • SDK displays "Mode: file"
  • Test endpoints produce logs
  • Correlation ID is present in logs
  • Stack traces are captured in error logs

Common Issues

IssueSolution
spdlog not foundInstall via vcpkg or apt-get
JSON formatting errorsEnsure nlohmann-json is properly installed
UUID generation failsInstall uuid-dev on Linux, or use Windows API
SDK can't read log fileCheck file permissions
Stack traces not capturedC++ exceptions don't include stack traces by default; use std::current_exception with backtrace libraries

Advanced: Capturing Full Stack Traces

For production stack traces, use:

#include <execinfo.h>
#include <cxxabi.h>

std::string getStackTrace() {
void* array[100];
size_t size = backtrace(array, 100);
char** symbols = backtrace_symbols(array, size);

std::string result;
for (size_t i = 0; i < size; i++) {
result += symbols[i];
result += "\n";
}
free(symbols);

return result;
}