@aws-amplify/core
Version:
Core category of aws-amplify
516 lines (441 loc) • 14 kB
text/typescript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import {
CloudWatchLogsClient,
CreateLogGroupCommand,
CreateLogGroupCommandInput,
CreateLogGroupCommandOutput,
CreateLogStreamCommand,
CreateLogStreamCommandInput,
CreateLogStreamCommandOutput,
DescribeLogGroupsCommand,
DescribeLogGroupsCommandInput,
DescribeLogGroupsCommandOutput,
DescribeLogStreamsCommand,
DescribeLogStreamsCommandInput,
DescribeLogStreamsCommandOutput,
GetLogEventsCommand,
GetLogEventsCommandInput,
GetLogEventsCommandOutput,
InputLogEvent,
LogGroup,
LogStream,
PutLogEventsCommand,
PutLogEventsCommandInput,
PutLogEventsCommandOutput,
} from '@aws-sdk/client-cloudwatch-logs';
import {
AWSCloudWatchProviderOptions,
CloudWatchDataTracker,
LoggingProvider,
} from '../types/types';
import { Credentials } from '../..';
import { ConsoleLogger as Logger } from '../Logger';
import { getAmplifyUserAgentObject } from '../Platform';
import { parseAWSExports } from '../parseAWSExports';
import {
AWS_CLOUDWATCH_BASE_BUFFER_SIZE,
AWS_CLOUDWATCH_CATEGORY,
AWS_CLOUDWATCH_MAX_BATCH_EVENT_SIZE,
AWS_CLOUDWATCH_MAX_EVENT_SIZE,
AWS_CLOUDWATCH_PROVIDER_NAME,
NO_CREDS_ERROR_STRING,
RETRY_ERROR_CODES,
} from '../Util/Constants';
const logger = new Logger('AWSCloudWatch');
class AWSCloudWatchProvider implements LoggingProvider {
static readonly PROVIDER_NAME = AWS_CLOUDWATCH_PROVIDER_NAME;
static readonly CATEGORY = AWS_CLOUDWATCH_CATEGORY;
private _config: AWSCloudWatchProviderOptions;
private _dataTracker: CloudWatchDataTracker;
private _currentLogBatch: InputLogEvent[];
private _timer;
private _nextSequenceToken: string | undefined;
constructor(config?: AWSCloudWatchProviderOptions) {
this.configure(config);
this._dataTracker = {
eventUploadInProgress: false,
logEvents: [],
};
this._currentLogBatch = [];
this._initiateLogPushInterval();
}
public getProviderName(): string {
return AWSCloudWatchProvider.PROVIDER_NAME;
}
public getCategoryName(): string {
return AWSCloudWatchProvider.CATEGORY;
}
public getLogQueue(): InputLogEvent[] {
return this._dataTracker.logEvents;
}
public configure(
config?: AWSCloudWatchProviderOptions
): AWSCloudWatchProviderOptions {
if (!config) return this._config || {};
const conf = Object.assign(
{},
this._config,
parseAWSExports(config).Logging,
config
);
this._config = conf;
return this._config;
}
public async createLogGroup(
params: CreateLogGroupCommandInput
): Promise<CreateLogGroupCommandOutput> {
logger.debug(
'creating new log group in CloudWatch - ',
params.logGroupName
);
const cmd = new CreateLogGroupCommand(params);
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (error) {
logger.error(`error creating log group - ${error}`);
throw error;
}
}
public async getLogGroups(
params: DescribeLogGroupsCommandInput
): Promise<DescribeLogGroupsCommandOutput> {
logger.debug('getting list of log groups');
const cmd = new DescribeLogGroupsCommand(params);
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (error) {
logger.error(`error getting log group - ${error}`);
throw error;
}
}
public async createLogStream(
params: CreateLogStreamCommandInput
): Promise<CreateLogStreamCommandOutput> {
logger.debug(
'creating new log stream in CloudWatch - ',
params.logStreamName
);
const cmd = new CreateLogStreamCommand(params);
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (error) {
logger.error(`error creating log stream - ${error}`);
throw error;
}
}
public async getLogStreams(
params: DescribeLogStreamsCommandInput
): Promise<DescribeLogStreamsCommandOutput> {
logger.debug('getting list of log streams');
const cmd = new DescribeLogStreamsCommand(params);
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (error) {
logger.error(`error getting log stream - ${error}`);
throw error;
}
}
public async getLogEvents(
params: GetLogEventsCommandInput
): Promise<GetLogEventsCommandOutput> {
logger.debug('getting log events from stream - ', params.logStreamName);
const cmd = new GetLogEventsCommand(params);
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (error) {
logger.error(`error getting log events - ${error}`);
throw error;
}
}
public pushLogs(logs: InputLogEvent[]): void {
logger.debug('pushing log events to Cloudwatch...');
this._dataTracker.logEvents = [...this._dataTracker.logEvents, ...logs];
}
private async _validateLogGroupExistsAndCreate(
logGroupName: string
): Promise<LogGroup> {
if (this._dataTracker.verifiedLogGroup) {
return this._dataTracker.verifiedLogGroup;
}
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const currGroups = await this.getLogGroups({
logGroupNamePrefix: logGroupName,
});
if (!(typeof currGroups === 'string') && currGroups.logGroups) {
const foundGroups = currGroups.logGroups.filter(
group => group.logGroupName === logGroupName
);
if (foundGroups.length > 0) {
this._dataTracker.verifiedLogGroup = foundGroups[0];
return foundGroups[0];
}
}
/**
* If we get to this point, it means that the specified log group does not exist
* and we should create it.
*/
await this.createLogGroup({ logGroupName });
return null;
} catch (err) {
const errString = `failure during log group search: ${err}`;
logger.error(errString);
throw err;
}
}
private async _validateLogStreamExists(
logGroupName: string,
logStreamName: string
): Promise<LogStream> {
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
const currStreams = await this.getLogStreams({
logGroupName,
logStreamNamePrefix: logStreamName,
});
if (currStreams.logStreams) {
const foundStreams = currStreams.logStreams.filter(
stream => stream.logStreamName === logStreamName
);
if (foundStreams.length > 0) {
this._nextSequenceToken = foundStreams[0].uploadSequenceToken;
return foundStreams[0];
}
}
/**
* If we get to this point, it means that the specified stream does not
* exist, and we should create it now.
*/
await this.createLogStream({
logGroupName,
logStreamName,
});
return null;
} catch (err) {
const errString = `failure during log stream search: ${err}`;
logger.error(errString);
throw err;
}
}
private async _sendLogEvents(
params: PutLogEventsCommandInput
): Promise<PutLogEventsCommandOutput> {
try {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK) {
throw new Error(NO_CREDS_ERROR_STRING);
}
logger.debug('sending log events to stream - ', params.logStreamName);
const cmd = new PutLogEventsCommand(params);
const client = this._initCloudWatchLogs();
const output = await client.send(cmd);
return output;
} catch (err) {
const errString = `failure during log push: ${err}`;
logger.error(errString);
}
}
private _initCloudWatchLogs() {
return new CloudWatchLogsClient({
region: this._config.region,
credentials: this._config.credentials,
customUserAgent: getAmplifyUserAgentObject(),
endpoint: this._config.endpoint,
});
}
private async _ensureCredentials() {
return await Credentials.get()
.then(credentials => {
if (!credentials) return false;
const cred = Credentials.shear(credentials);
logger.debug('set credentials for logging', cred);
this._config.credentials = cred;
return true;
})
.catch(error => {
logger.warn('ensure credentials error', error);
return false;
});
}
private async _getNextSequenceToken(): Promise<string> {
if (this._nextSequenceToken && this._nextSequenceToken.length > 0) {
return this._nextSequenceToken;
}
/**
* A sequence token will not exist if any of the following are true:
* ...the log group does not exist
* ...the log stream does not exist
* ...the log stream does exist but has no logs written to it yet
*/
try {
await this._validateLogGroupExistsAndCreate(this._config.logGroupName);
this._nextSequenceToken = undefined;
const logStream = await this._validateLogStreamExists(
this._config.logGroupName,
this._config.logStreamName
);
if (logStream) {
this._nextSequenceToken = logStream.uploadSequenceToken;
}
return this._nextSequenceToken;
} catch (err) {
logger.error(`failure while getting next sequence token: ${err}`);
throw err;
}
}
private async _safeUploadLogEvents(): Promise<PutLogEventsCommandOutput> {
try {
/**
* CloudWatch has restrictions on the size of the log events that get sent up.
* We need to track both the size of each event and the total size of the batch
* of logs.
*
* We also need to ensure that the logs in the batch are sorted in chronological order.
* https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
*/
const seqToken = await this._getNextSequenceToken();
const logBatch =
this._currentLogBatch.length === 0
? this._getBufferedBatchOfLogs()
: this._currentLogBatch;
const putLogsPayload: PutLogEventsCommandInput = {
logGroupName: this._config.logGroupName,
logStreamName: this._config.logStreamName,
logEvents: logBatch,
sequenceToken: seqToken,
};
this._dataTracker.eventUploadInProgress = true;
const sendLogEventsResponse = await this._sendLogEvents(putLogsPayload);
this._nextSequenceToken = sendLogEventsResponse.nextSequenceToken;
this._dataTracker.eventUploadInProgress = false;
this._currentLogBatch = [];
return sendLogEventsResponse;
} catch (err) {
logger.error(`error during _safeUploadLogEvents: ${err}`);
if (RETRY_ERROR_CODES.includes(err.name)) {
this._getNewSequenceTokenAndSubmit({
logEvents: this._currentLogBatch,
logGroupName: this._config.logGroupName,
logStreamName: this._config.logStreamName,
});
} else {
this._dataTracker.eventUploadInProgress = false;
throw err;
}
}
}
private _getBufferedBatchOfLogs(): InputLogEvent[] {
/**
* CloudWatch has restrictions on the size of the log events that get sent up.
* We need to track both the size of each event and the total size of the batch
* of logs.
*
* We also need to ensure that the logs in the batch are sorted in chronological order.
* https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
*/
let currentEventIdx = 0;
let totalByteSize = 0;
while (currentEventIdx < this._dataTracker.logEvents.length) {
const currentEvent = this._dataTracker.logEvents[currentEventIdx];
const eventSize = currentEvent
? new TextEncoder().encode(currentEvent.message).length +
AWS_CLOUDWATCH_BASE_BUFFER_SIZE
: 0;
if (eventSize > AWS_CLOUDWATCH_MAX_EVENT_SIZE) {
const errString = `Log entry exceeds maximum size for CloudWatch logs. Log size: ${eventSize}. Truncating log message.`;
logger.warn(errString);
currentEvent.message = currentEvent.message.substring(0, eventSize);
}
if (totalByteSize + eventSize > AWS_CLOUDWATCH_MAX_BATCH_EVENT_SIZE)
break;
totalByteSize += eventSize;
currentEventIdx++;
}
this._currentLogBatch = this._dataTracker.logEvents.splice(
0,
currentEventIdx
);
return this._currentLogBatch;
}
private async _getNewSequenceTokenAndSubmit(
payload: PutLogEventsCommandInput
): Promise<PutLogEventsCommandOutput> {
try {
this._nextSequenceToken = undefined;
this._dataTracker.eventUploadInProgress = true;
const seqToken = await this._getNextSequenceToken();
payload.sequenceToken = seqToken;
const sendLogEventsRepsonse = await this._sendLogEvents(payload);
this._dataTracker.eventUploadInProgress = false;
this._currentLogBatch = [];
return sendLogEventsRepsonse;
} catch (err) {
logger.error(
`error when retrying log submission with new sequence token: ${err}`
);
this._dataTracker.eventUploadInProgress = false;
throw err;
}
}
private _initiateLogPushInterval(): void {
if (this._timer) {
clearInterval(this._timer);
}
this._timer = setInterval(async () => {
try {
if (this._getDocUploadPermissibility()) {
await this._safeUploadLogEvents();
}
} catch (err) {
logger.error(
`error when calling _safeUploadLogEvents in the timer interval - ${err}`
);
}
}, 2000);
}
private _getDocUploadPermissibility(): boolean {
return (
(this._dataTracker.logEvents.length !== 0 ||
this._currentLogBatch.length !== 0) &&
!this._dataTracker.eventUploadInProgress
);
}
}
export { AWSCloudWatchProvider };