Files
UnrealPrototyping/Source/SaltyCore/Telemetry/TelemetrySink.h
T
Rainer LeitandClaude Fable 5.1 3e52d26fb7 Step 2: the telemetry seam
ITelemetrySink with the null, log and JSON Lines sinks, FTelemetryEvent and the envelope, TelemetryEvents,
UTelemetrySubsystem on the game instance, ASaltyGameMode minting the session id into the replicated
ASaltyGameState, bs.TelemetryTest, the git hash in the build string (D-42) and four Salty.Core.Telemetry tests
replacing the placeholder. Proved standalone and with a headless server plus client sharing one session id.

Also enables the engine's Editor, AutomationTest, GameplayTags, ConfigSettings and LiveCoding MCP toolsets
(D-43) so the editor's MCP server exposes more than the skills toolset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:22:20 +03:00

70 lines
2.3 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "Containers/Queue.h"
#include "HAL/Runnable.h"
#include "Telemetry/TelemetryEvent.h"
class FRunnableThread;
class FEvent;
class IFileHandle;
// Where events go. The emitter never knows. Three implementations on day one; a fourth adapts an analytics
// provider when there is one (Docs/Spec/Telemetry.md, Q2).
class SALTYCORE_API ITelemetrySink
{
public:
virtual ~ITelemetrySink() = default;
virtual void Emit(const FTelemetryEnvelope& Envelope, const FTelemetryEvent& Event) = 0;
virtual void Flush() {}
};
// Does nothing. The shipping default until there is somewhere to send anything.
class SALTYCORE_API FNullTelemetrySink : public ITelemetrySink
{
public:
virtual void Emit(const FTelemetryEnvelope&, const FTelemetryEvent&) override {}
};
// One line of JSON to the output log under LogTelemetry. Verifies a step emitted what it claims.
class SALTYCORE_API FLogTelemetrySink : public ITelemetrySink
{
public:
virtual void Emit(const FTelemetryEnvelope& Envelope, const FTelemetryEvent& Event) override;
};
// Appends one line per event to a JSON Lines file. Emit only builds the line and queues it; a worker thread
// drains the queue to disk every DrainIntervalSeconds and on Flush, so the calling thread never touches the
// file. Flushed on quit and from the unhandled-exception handler, so a crash loses at most one interval.
class SALTYCORE_API FJsonlTelemetrySink : public ITelemetrySink, private FRunnable
{
public:
explicit FJsonlTelemetrySink(const FString& InFilePath, double DrainIntervalSeconds = 2.0);
virtual ~FJsonlTelemetrySink() override;
virtual void Emit(const FTelemetryEnvelope& Envelope, const FTelemetryEvent& Event) override;
virtual void Flush() override;
const FString& GetFilePath() const { return FilePath; }
private:
// FRunnable
virtual uint32 Run() override;
virtual void Stop() override;
void Drain();
FString FilePath;
double DrainIntervalSeconds;
TQueue<FString, EQueueMode::Mpsc> Queue;
TUniquePtr<IFileHandle> File;
FEvent* WakeEvent = nullptr;
FEvent* DrainedEvent = nullptr;
FRunnableThread* Thread = nullptr;
TAtomic<bool> bStopping{ false };
TAtomic<uint64> FlushesRequested{ 0 };
TAtomic<uint64> FlushesCompleted{ 0 };
FDelegateHandle OnExitHandle;
FDelegateHandle OnSystemErrorHandle;
};