---
title: "Logs and metrics with CloudWatch"
description: "Send logs and metrics and connect CloudWatch to game telemetry."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Finish [AWS setup](/integrations/aws/setup). Set **Log Group**, **Log Stream**, and **Metric Namespace** under **AWS CloudWatch**. Create the log group and stream, and grant the runtime role permission to write logs and metrics.

Use the [Crossplay telemetry flow](/crossplay/telemetry) for player consent. Calling CloudWatch's raw nodes directly does not pass through that consent gate.

## Record a metric

**Blueprint**

<Accordion>
  <AccordionItem title="Record match duration">
<BlueprintViewer title="Record match duration" src="/blueprints/crossplay-telemetry-metric.txt" />
  </AccordionItem>
  <AccordionItem title="Flush queued records">
<BlueprintViewer title="Flush queued records" src="/blueprints/crossplay-telemetry-flush.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `UCIKCore`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Telemetry/UCIKTelemetrySubsystem.h"
#include "UCIKCore.h"

void RecordMatchDuration(UGameInstance &GameInstance, double DurationSeconds)
{
    auto *Telemetry = GameInstance.GetSubsystem<UUCIKTelemetrySubsystem>();
    if (!Telemetry || !Telemetry->HasConsent())
        return;
    const FBetideError Error =
        Telemetry->RecordMetric(TEXT("MatchDurationSeconds"), DurationSeconds, {});
    if (Error.IsError())
    {
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Error.GetLogString());
    }
}

void FlushGameTelemetry(UGameInstance &GameInstance)
{
    auto *Telemetry = GameInstance.GetSubsystem<UUCIKTelemetrySubsystem>();
    if (!Telemetry)
        return;
    Telemetry->Flush(
        FBetideCallback<bool>::CreateWeakLambda(&GameInstance, [](const TBetideResult<bool> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            UE_LOG(LogUCIK, Log, TEXT("Telemetry flushed: %d"), Result.GetValue());
        }));
}
```

Records are queued in memory. Call **Flush Crossplay Telemetry** at a checkpoint and wait for its result before treating delivery as complete. Do not rely on process exit to finish an upload.

<Accordion>
  <AccordionItem title="Privacy, batching, and raw service calls">

**Allow PII** defaults off and rejects events flagged as personal data. The CloudWatch adapter also redacts JSON fields whose names indicate secrets, tokens, email, or credentials, including nested fields. This is a key-name filter, not a guarantee that arbitrary payload text is safe.

Use a JSON object for events. Metrics require a finite value, a 1–255 character name, and at most 30 dimensions; sensitive dimension names are rejected. **Max Batch Items** controls request size. A full queue returns an error, so handle record failures as well as flush failures.

All processes using one configuration write to its configured log stream. Choose destinations deliberately for each environment. A second overlapping flush is rejected; wait for completion before starting another.

For direct service access, use **Amazon CloudWatch Logs Put Log Events** or **Amazon CloudWatch Metrics Put Metric Data** with their generated request types in `AWSIKCloudWatch`. These bypass UCIK's consent and structured-event handling.

  </AccordionItem>
</Accordion>

## Direct server diagnostics

These examples send directly to CloudWatch. Set your own destinations in the request; the raw nodes do not read the telemetry adapter's Log Group, Log Stream, or Metric Namespace settings. Create `/mygame/server` and `dev-server-01` first, or replace them.

**Blueprint**

<Accordion>
  <AccordionItem title="Upload a server queue-time metric">
<BlueprintViewer title="Upload a server queue-time metric" src="/blueprints/crossplay-aws-cloudwatch-metric.txt" />
  </AccordionItem>
  <AccordionItem title="Upload a server-ready log">
<BlueprintViewer title="Upload a server-ready log" src="/blueprints/crossplay-aws-cloudwatch-log.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `AWSIKCloudWatch`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Metrics/Nodes/AWSIKCloudWatchMetricsMetricsNodes03.h"
#include "Generated/Events/Nodes/AWSIKCloudWatchLogsEventsNodes05.h"

void UploadServerQueueTime(UGameInstance &GameInstance, double QueueSeconds,
                           const FScriptDelegate &OnComplete)
{
    FAWSIKCloudWatchMetricsMetricDatum Metric;
    Metric.MetricName = TEXT("QueueTimeSeconds");
    Metric.bSetValue = true;
    Metric.Value = QueueSeconds;
    Metric.bSetUnit = true;
    Metric.Unit = EAWSIKCloudWatchMetricsStandardUnit::Seconds;
    FAWSIKCloudWatchMetricsPutMetricDataRequest Request;
    Request.Namespace = TEXT("MyGame/Server");
    Request.bSetMetricData = true;
    Request.MetricData.Add(MoveTemp(Metric));
    auto *Action = UAWSIKCloudWatchMetricsPutMetricData::PutMetricData(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

void UploadServerReadyLog(UGameInstance &GameInstance, const FScriptDelegate &OnComplete)
{
    FAWSIKCloudWatchLogsInputLogEvent Event;
    Event.timestamp = FDateTime::UtcNow().ToUnixTimestamp() * 1000;
    Event.message = TEXT("{\"event\":\"ServerReady\",\"map\":\"Arena\"}");
    FAWSIKCloudWatchLogsPutLogEventsRequest Request;
    Request.logGroupName = TEXT("/mygame/server");
    Request.logStreamName = TEXT("dev-server-01");
    Request.logEvents.Add(MoveTemp(Event));
    auto *Action = UAWSIKCloudWatchLogsPutLogEvents::PutLogEvents(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

bool WereLogEventsRejected(const FAWSIKCloudWatchLogsPutLogEventsResult &Result)
{
    const auto &Rejected = Result.rejectedLogEventsInfo;
    return Rejected.bSettooNewLogEventStartIndex || Rejected.bSettooOldLogEventEndIndex ||
           Rejected.bSetexpiredLogEventEndIndex;
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKCloudWatchMetricsEmptyResult&` (metrics) or `const FAWSIKCloudWatchLogsPutLogEventsResult&` (logs), then `const FAWSIKCloudWatchError&`. Check `Error.IsError()` first and display `Error.Common.Message`. For logs, also check `WereLogEventsRejected(Result)` before reporting delivery.

Keep **Value**, **Unit**, and **Metric Data** enabled on the Make nodes. C++ sets their `bSet...` flags explicitly. Use a finite number for queue time; the metric timestamp is omitted so CloudWatch supplies it. New metrics can take time to appear in listings. [Metric API details](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html).

Log timestamps use Unix **milliseconds**. The example uploads one event and checks the three rejection flags; when batching, use their indices to identify rejected entries and sort events by timestamp. CloudWatch ignores sequence tokens. [Log API details](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html).
