웹 앱용 CloudWatch 로깅(3부)

이전 두 게시물에서는 프론트엔드 앱에서 CloudWatch에 로그인한 다음 REST API에서 로그인하는 REST API를 만드는 방법을 다루었습니다. 이 시리즈의 마지막 게시물은 로깅 REST API를 사용하기 위한 프론트엔드 클라이언트의 구현을 다룹니다.

개요



프런트엔드 클라이언트는 각 앱 세션에 대한 CloudWatch 로그 스트림을 생성한 다음 순차적 배치로 로그 메시지를 작성하는 역할을 합니다. 첫 번째 배치가 아닌 경우 각 배치에는 이전 배치의 시퀀스 번호가 포함되어야 합니다.

LambdaSharp로 빌드된 Blazor WebAssembly 앱의 경우 클라이언트 구현은 LambdaSharpAppClient 클래스에 있습니다. 클래스는 싱글톤으로 인스턴스화되며 ILogger 인터페이스를 통해 직접 또는 간접적으로 주입할 수 있습니다. 프런트엔드 앱이 CloudWatch에 기록하려면 다음 문 중 하나만 필요합니다. 선택은 개인 취향에 달려 있습니다.

@inject LambdaSharpAppClient AppClient
@inject ILogger<Index> Logger

구현



CloudWatch Logs는 로그 항목을 로그 스트림으로 구성합니다. 로그 스트림은 항목의 시간순입니다. 많은 로그 스트림이 동시에 존재할 수 있습니다. Blazor WebAssembly 앱 및 기타 단일 페이지 앱의 경우 첫 번째 로그 메시지가 생성될 때 요청 시 로그 스트림을 만드는 것이 좋습니다.

이 게시물의 코드는 단순화를 위해 수정되었습니다. 실제 구현에서는 주의를 산만하게 만드는 몇 가지 예외적인 경우를 더 다룹니다. 완전한 구현을 찾을 수 있습니다here.

로그 메시지 보내기



앱 클라이언트는 내부 누적기에 메시지를 큐에 넣습니다. 이를 통해 구현이 제한을 피하기 위해 한 번에 여러 메시지를 보낼 수 있습니다.

private readonly List<PutLogEventsRequestEntry> _logs = new List<PutLogEventsRequestEntry>();
private void SendMessage(string message) {

    // queue message for server-side logging
    _logs.Add(new PutLogEventsRequestEntry {
        Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
        Message = message ?? throw new ArgumentNullException(nameof(message))
    });
}

시간 누적기



누산기는 대기 중인 메시지에 대한 타이머에 의해 매초 확인됩니다. 타이머 콜백은 먼저 이전 비동기 작업이 완료되었는지 확인합니다. 그런 다음 누적된 메시지를 플러시하려고 시도합니다.

private Task _previousOperationTask;
private void OnTimer(object _) {
    if(!(_previousOperationTask?.IsCompleted ?? true)) {

        // previous operation is still going; wait until next timer invocation to proceed
        return;
    }

    // initialize invocation to FlushAsync(), but don't wait for it to finish
    _previousOperationTask = FlushAsync();
}

일괄 전송



누적된 메시지의 첫 번째 배치를 보내기 전에 클라이언트는 로그 스트림이 생성되었는지 확인해야 합니다. 그런 다음 누적된 메시지를 1MB 크기 또는 10,000개 메시지 중 더 작은 것으로 제한된 배치로 청크합니다. 클라이언트가 오프라인일 가능성이 있기 때문에 작업이 실패하면 메시지가 누적기에 다시 삽입됩니다.

private string _logStreamName;
private string _sequenceToken;
private async Task FlushAsync() {

    // check if any messages are pending
    if(!_logs.Any()) {
        return;
    }

    // check if a log stream must be created
    if(_logStreamName == null) {
        _logStreamName = AppInstanceId;
        var response = await CreateLogStreamAsync(new CreateLogStreamRequest {
            LogStreamName = _logStreamName
        });
        if(response.Error != null) {
            Console.WriteLine($"*** ERROR: unable to create log stream: {_logStreamName} (Error: {response.Error})");
            return;
        }
    }


    // NOTE (2020-08-06, bjorg): we limit the number of log message we send in the unlikely event that we have too many
    //  See: https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
    const int MaxPayloadSize = 1_048_576;
    const int MaxMessageCount = 10_000;

    // consume as many accumulated log messages as possible
    var payloadByteCount = 0;
    var logs = _logs.Take(MaxMessageCount).TakeWhile(log => {
        var logMessageByteCount = Encoding.UTF8.GetByteCount(log.Message) + 26;
        if((payloadByteCount + logMessageByteCount) >= MaxPayloadSize) {
            return false;
        }
        payloadByteCount += logMessageByteCount;
        return true;
    }).ToList();
    _logs.RemoveRange(0, logs.Count);

    // send log messages to CloudWatch Logs
    try {
        var response = await PutLogEventsAsync(new PutLogEventsRequest {
            LogStreamName = _logStreamName,
            LogEvents = logs,
            SequenceToken = _sequenceToken
        });

        // on error, re-insert the log messages and try again later
        if(response.Error != null) {
            _logs.InsertRange(0, logs);
            return;
        }
        _sequenceToken = response.NextSequenceToken;
    } catch {

        // on exception, re-insert the log messages and try again later
        _logs.InsertRange(0, logs);
    }
}

처분



마지막으로, 클라이언트는 누적기에서 보류 중인 모든 메시지가 CloudWatch로 전송되도록 하기 위해 폐기될 때 최종 플러시 작업을 수행합니다.

async ValueTask IAsyncDisposable.DisposeAsync() {

    // stop timer and wait for any lingering timer operations to finish
    await _timer.DisposeAsync();

    // wait for any in-flight operation to complete
    if(!(_previousOperationTask?.IsCompleted ?? true)) {
        await _previousOperationTask;
    }

    // flush all remaining messages
    while(_logs.Any()) {
        await FlushAsync();
    }
}

결론



그게 다야 LambdaSharp가 Blazor WebAssembly 프런트엔드 앱에 대해 CloudWatch Logs 지원을 구현한 방법에 대한 이 비하인드 스토리 시리즈를 즐겼기를 바랍니다. 자신의 노력에 유용할 수 있기를 바랍니다.

해피 해킹!

좋은 웹페이지 즐겨찾기