Skip to content

Instantly share code, notes, and snippets.

@Solessfir
Last active November 30, 2025 14:42
Show Gist options
  • Select an option

  • Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.

Select an option

Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.
C++23 std::print-style Single Header Logging library for Unreal Engine 5.2+
/**
* Copyright (c) Solessfir under MIT license
*
* #define EASY_LOG_CATEGORY LogMyGame // Optional (must be defined before include)
* #include "EasyLog.h"
*
* Usage Examples:
*
* // Basic
* LOG_DISPLAY("Value is {}", MyInt);
*
* // Extended (Key, Duration)
* LOG_DISPLAY_EX(-1, 5.f, "Value is {}", MyInt);
*
* // Positional args
* LOG_DISPLAY("Value is {1}, expected {0}", Foo, Bar); // Value is Bar, expected Foo
*
* // Formatting Specifiers
* LOG_DISPLAY("Int: {:03}", 7); // Output: Int: 007
* LOG_DISPLAY("Float: {:.2}", 3.14159); // Output: Float: 3.14
*
* // Hex and Binary
* int32 Val = 255;
* LOG_DISPLAY("Hex: {:#x}", Val); // Output: Hex: 0xff
* LOG_DISPLAY("Bin: {:#b}", Val); // Output: Bin: 0b11111111
*
* // Pointer Address
* LOG_DISPLAY("Ptr: {:#x}", this); // Output: Ptr: 0x00...
*
* // Container Support (TArray, TSet)
* TArray<float> Values = {1.11f, 2.22f};
* LOG_DISPLAY("Values: {:.1}", Values); // Output: Values: [1.1, 2.2]
*
* // UEnum and FGameplayTag Support
* LOG_DISPLAY("State: {}", EMyEnum::Walking);
* LOG_DISPLAY("Tag: {}", MyTag);
*
* // Conditional Logging
* CLOG_ERROR(Health <= 0, "Actor {} died!", this);
*/
#pragma once
#include "CoreMinimal.h"
#include "Engine/Engine.h"
#include "Logging/StructuredLog.h"
#include <source_location>
#include <concepts>
#include <cstring>
#include <cstdarg>
#ifndef EASY_LOG_CATEGORY
#define EASY_LOG_CATEGORY EasyLog
#endif
// Helper to expand macro before definition
#define EASY_LOG_DEFINE_CATEGORY_INTERNAL(Category) DEFINE_LOG_CATEGORY_STATIC(Category, Log, All)
EASY_LOG_DEFINE_CATEGORY_INTERNAL(EASY_LOG_CATEGORY);
#undef EASY_LOG_DEFINE_CATEGORY_INTERNAL
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
#define LOG_DISPLAY(Format, ...) \
Easy::Log<ELogVerbosity::Display>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define LOG_WARNING(Format, ...) \
Easy::Log<ELogVerbosity::Warning>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define LOG_ERROR(Format, ...) \
Easy::Log<ELogVerbosity::Error>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define CLOG_DISPLAY(Condition, Format, ...) \
if (Condition) Easy::Log<ELogVerbosity::Display>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define CLOG_WARNING(Condition, Format, ...) \
if (Condition) Easy::Log<ELogVerbosity::Warning>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define CLOG_ERROR(Condition, Format, ...) \
if (Condition) Easy::Log<ELogVerbosity::Error>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define LOG_DISPLAY_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Display>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#define LOG_WARNING_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Warning>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#define LOG_ERROR_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Error>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#else
#define LOG_DISPLAY(...)
#define LOG_WARNING(...)
#define LOG_ERROR(...)
#define CLOG_DISPLAY(...)
#define CLOG_WARNING(...)
#define CLOG_ERROR(...)
#define LOG_DISPLAY_EX(...)
#define LOG_WARNING_EX(...)
#define LOG_ERROR_EX(...)
#endif
namespace Easy
{
// Concepts
template<typename ArgType> concept CIsStringArg = std::constructible_from<FStringFormatArg, ArgType>;
template<typename ArgType> concept CHasToString = requires(ArgType t) { { t.ToString() } -> std::convertible_to<FString>; };
template<typename ArgType> concept CIsActor = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<ArgType>>, AActor>;
template<typename ArgType> concept CIsUObject = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<ArgType>>, UObject>;
template<typename ArgType> concept CIsUEnum = std::is_enum_v<std::remove_cvref_t<ArgType>> && requires { StaticEnum<std::remove_cvref_t<ArgType>>(); };
template<typename ArgType> concept CIsRawEnum = std::is_enum_v<std::remove_cvref_t<ArgType>> && !CIsUEnum<ArgType>;
template<typename ArgType> concept CIsContainer = requires(ArgType t) { std::begin(t); std::end(t); } && !CIsStringArg<ArgType> && !std::is_same_v<ArgType, FString>;
struct FParsedFormat
{
FString FixedString;
TArray<FString> Specifiers;
};
struct FormatStringHelper
{
static FParsedFormat Parse(const FString& InFormat)
{
if (!InFormat.Contains(TEXT("{")))
{
return { InFormat, {} };
}
FParsedFormat Result;
Result.FixedString.Reserve(InFormat.Len() + 16);
int32 ArgIndex = 0;
const int32 Len = InFormat.Len();
for (int32 i = 0; i < Len; ++i)
{
const TCHAR Char = InFormat[i];
if (Char == '{')
{
if (i + 1 < Len && InFormat[i + 1] == '{')
{
Result.FixedString.AppendChar('{'); Result.FixedString.AppendChar('{'); i++;
continue;
}
int32 CloseIdx = -1;
for (int32 j = i + 1; j < Len; ++j)
{
if (InFormat[j] == '}')
{
CloseIdx = j;
break;
}
}
if (CloseIdx != -1)
{
FString Content = InFormat.Mid(i + 1, CloseIdx - (i + 1));
FString Specifier;
int32 ColonIdx;
// Extract specifier (e.g. "02" from "{:02}" or "{0:02}")
if (Content.FindChar(TEXT(':'), ColonIdx))
{
Specifier = Content.RightChop(ColonIdx + 1);
Specifier.TrimStartAndEndInline();
Content = Content.Left(ColonIdx);
}
if (Content.IsEmpty())
{
// Case: {} or {:spec} -> Auto-Increment Index
Result.FixedString.AppendChar('{'); Result.FixedString.AppendInt(ArgIndex); Result.FixedString.AppendChar('}');
if (Result.Specifiers.Num() <= ArgIndex) Result.Specifiers.SetNum(ArgIndex + 1);
Result.Specifiers[ArgIndex] = Specifier;
ArgIndex++;
}
else if (Content.IsNumeric())
{
// Case: {0} or {0:spec} -> Explicit Index
Result.FixedString.AppendChar('{'); Result.FixedString += Content; Result.FixedString.AppendChar('}');
const int32 ExplicitIdx = FCString::Atoi(*Content);
if (Result.Specifiers.Num() <= ExplicitIdx) Result.Specifiers.SetNum(ExplicitIdx + 1);
Result.Specifiers[ExplicitIdx] = Specifier;
}
else
{
// Unsupported Case: {Name} -> Treat as literal text
// Append the entire original block including braces
Result.FixedString.AppendChars(&InFormat[i], CloseIdx - i + 1);
}
i = CloseIdx;
continue;
}
}
Result.FixedString.AppendChar(Char);
}
return Result;
}
};
struct SafeFormatter
{
static FString RunFormat(const TCHAR* Fmt, ...)
{
TCHAR Buffer[128];
va_list Ap;
va_start(Ap, Fmt);
FCString::GetVarArgs(Buffer, 128, Fmt, Ap);
va_end(Ap);
return FString(Buffer);
}
static FString ToBinary(const int64 Value, const bool bUsePrefix)
{
FString Result;
uint64 UVal = static_cast<uint64>(Value);
if (UVal == 0)
{
Result = TEXT("0");
}
else
{
while (UVal > 0)
{
Result = (UVal & 1 ? TEXT("1") : TEXT("0")) + Result;
UVal >>= 1;
}
}
return bUsePrefix ? TEXT("0b") + Result : Result;
}
static FString FormatInt(const int64 Value, const FString& Spec)
{
if (Spec.EndsWith(TEXT("b"), ESearchCase::IgnoreCase))
{
return ToBinary(Value, Spec.Contains(TEXT("#")));
}
if (Spec.EndsWith(TEXT("x"), ESearchCase::IgnoreCase))
{
FString HexSpec = Spec;
if (HexSpec.Contains(TEXT("x"))) HexSpec.ReplaceInline(TEXT("x"), TEXT("llx"));
else if (HexSpec.Contains(TEXT("X"))) HexSpec.ReplaceInline(TEXT("X"), TEXT("llX"));
const FString Fmt = TEXT("%") + HexSpec;
return RunFormat(*Fmt, Value);
}
const FString Fmt = TEXT("%") + Spec + TEXT("lld");
return RunFormat(*Fmt, Value);
}
static FString FormatFloat(const double Value, const FString& Spec)
{
const FString Fmt = TEXT("%") + Spec + TEXT("f");
return RunFormat(*Fmt, Value);
}
};
// Argument Formatting
template <typename ArgType>
FString ElementToString(const ArgType& Value, const FString& Spec = FString())
{
using RawType = std::remove_cvref_t<ArgType>;
// Literal nullptr
if constexpr (std::is_same_v<RawType, std::nullptr_t>) return TEXT("nullptr");
// Pointers (Check for address formatting request)
else if constexpr (std::is_pointer_v<RawType>)
{
// If format string provided (e.g. {:#x}), treat pointer address as int64 and format it
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatInt(reinterpret_cast<int64>(Value), Spec);
}
// Otherwise standard pointer handling
if constexpr (CIsUObject<RawType>)
{
if (IsInGameThread() && IsValid(Value))
{
if constexpr (CIsActor<RawType>) return Value->GetActorNameOrLabel();
return Value->GetName();
}
return IsValid(Value) ? TEXT("[AsyncUObject]") : TEXT("None");
}
else if constexpr (std::is_same_v<std::remove_pointer_t<RawType>, TCHAR>)
{
return Value ? FString(Value) : TEXT("(NullString)");
}
else
{
// Generic pointer default format
return FString::Printf(TEXT("0x%p"), Value);
}
}
// Float
else if constexpr (std::is_floating_point_v<RawType>)
{
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatFloat(static_cast<double>(Value), Spec);
}
return FString::SanitizeFloat(Value);
}
// Integer and Enum (Raw)
else if constexpr (std::is_integral_v<RawType> && !std::is_same_v<RawType, bool>)
{
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatInt(static_cast<int64>(Value), Spec);
}
return LexToString(Value);
}
// UEnum
else if constexpr (CIsUEnum<RawType>)
{
if (const UEnum* EnumPtr = StaticEnum<RawType>())
{
return EnumPtr->GetNameStringByValue(static_cast<int64>(Value));
}
return LexToString(static_cast<int64>(Value));
}
// Standard
else if constexpr (CIsStringArg<ArgType>) return LexToString(Value);
else if constexpr (CHasToString<ArgType>) return Value.ToString();
else
{
return TEXT("[?]");
}
}
template <typename ArgType>
FString ContainerToString(const ArgType& Container, const FString& Spec)
{
FString Result = TEXT("[");
int32 Count = 0;
constexpr int32 MaxElements = 15;
for (const auto& Elem : Container)
{
if (Count > 0) Result += TEXT(", ");
if (Count >= MaxElements) { Result += TEXT("..."); break; }
Result += ElementToString(Elem, Spec);
Count++;
}
return Result + TEXT("]");
}
struct FSourceLoc
{
FString Function;
int32 Hash;
};
consteval std::source_location Loc(const std::source_location& Location = std::source_location::current())
{
return Location;
}
inline FSourceLoc ProcessLocation(const std::source_location& Location)
{
const char* FnAnsi = Location.function_name();
uint32 Hash = FCrc::MemCrc32(FnAnsi, std::strlen(FnAnsi));
Hash = (Hash ^ (Location.line() << 15)) & 0x7FFFFFFF;
FString FnName(ANSI_TO_TCHAR(FnAnsi));
int32 Index;
if (FnName.FindLastChar(TEXT(' '), Index)) FnName = FnName.RightChop(Index + 1);
if (FnName.FindChar(TEXT('('), Index)) FnName = FnName.Left(Index);
return { FString::Printf(TEXT("%s:%d"), *FnName, Location.line()), static_cast<int32>(Hash) };
}
// Argument Processor
inline void FillArgs(FStringFormatOrderedArguments&, const TArray<FString>&, int32) {}
template<typename FirstArgType, typename... RestArgsType>
void FillArgs(FStringFormatOrderedArguments& Args, const TArray<FString>& Specs, const int32 Index, FirstArgType&& First, RestArgsType&&... Rest)
{
FString CurrentSpec = (Specs.IsValidIndex(Index)) ? Specs[Index] : FString();
if constexpr (CIsContainer<std::remove_cvref_t<FirstArgType>>)
{
Args.Emplace(ContainerToString(First, CurrentSpec));
}
else
{
Args.Emplace(ElementToString(First, CurrentSpec));
}
FillArgs(Args, Specs, Index + 1, std::forward<RestArgsType>(Rest)...);
}
// Helper for Output Log
template <ELogVerbosity::Type Verbosity>
void LogToConsole(const FString& Message)
{
if constexpr (Verbosity == ELogVerbosity::Fatal) UE_LOGFMT(EASY_LOG_CATEGORY, Fatal, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Error) UE_LOGFMT(EASY_LOG_CATEGORY, Error, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Warning) UE_LOGFMT(EASY_LOG_CATEGORY, Warning, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Display) UE_LOGFMT(EASY_LOG_CATEGORY, Display, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Verbose) UE_LOGFMT(EASY_LOG_CATEGORY, Verbose, "{0}", Message);
else UE_LOGFMT(EASY_LOG_CATEGORY, Log, "{0}", Message);
}
// Main Log Functions
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Log(const std::source_location& SourceLocation, const float Duration, const FString& Format, ArgsType&&... Arguments)
{
const auto [Function, Hash] = ProcessLocation(SourceLocation);
const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format);
FStringFormatOrderedArguments OrderedArgs;
FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...);
const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs));
const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Function);
LogToConsole<Verbosity>(FullMessage);
if (GEngine && Duration > 0.f)
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Hash, Duration, Color, UserMessage);
}
}
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Log(const int32 Key, const float Duration, const std::source_location& SourceLocation, const FString& Format, ArgsType&&... Arguments)
{
const auto [Function, Hash] = ProcessLocation(SourceLocation);
const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format);
FStringFormatOrderedArguments OrderedArgs;
FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...);
const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs));
const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Function);
LogToConsole<Verbosity>(FullMessage);
if (GEngine && Duration > 0.f)
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment