This post goes through an example setup of client-side log and analytics collection from authenticated and guest clients, using AWS services.
The work will be split into two parts :
- Infrastructure Setup : Creating the required infrastructure using the AWS CDK
- Client-Side Integration : Interacting with AWS APIs from the client
The following AWS services will be used
- Amazon Cognito - Authentication
- Amazon CloudWatch - Application and Infrastructure Monitoring
- Amazon Pinpoint - Customer Engagement
The client will retrieve temporary AWS credentials using Amazon Cognito, and use these credentials to log events to CloudWatch and Pinpoint.
Notes :
If you are using / can use Amplify, you don't need any of this, the nice folks there have got you covered : just add Auth and Analytics categories and you're good to go. Amplify Docs
This post is just a wrap-up of my experience playing with these services for my future recollection. Please do not treat this as official advice in any way.
Just show me the code, please !
Infrastructure Setup
This solution doesn't add much infrastructure to maintain, here's what we need :
- A Cognito Identity Pool (with unauthenticated guest access)
- A Cognito User Pool
- An IAM Role for authenticated users
- An IAM Role for un-authenticated users
For this post, a similar IAM role will be provided for both.
The IAM role will be granted to all visitors so the permissions that will be granted need to be as minimal as possible.
The following permissions will be given :
-
logs:CreateLogStream
- Each user needs to create their own log stream. The log group is created by the admin account. -
logs:PutLogEvents
- Allows user to send logs to cloudwatch -
mobiletargeting:PutEvents
- Allows user to send events to Amazon Pinpoint
This can be done using the AWS console but let's use the CDK to commit our infrastructure as code.
Example TypeScript code can be found here
// Create resources
const userPool = new cognito.UserPool(this, "user-pool", {});
const userPoolClient = new cognito.UserPoolClient(this, "UserPoolClient", {
userPool,
generateSecret: false, // Don't need to generate secret for web app running on browsers
});
const identityPool = new cognito.CfnIdentityPool(this, "IdentityPool", {
allowUnauthenticatedIdentities: true, // Allow unathenticated users
cognitoIdentityProviders: [
{
clientId: userPoolClient.userPoolClientId,
providerName: userPool.userPoolProviderName,
},
],
});
const pinpointApp = new pinpoint.CfnApp(this, "PinpointApp", {
name: `pinpoint-${identityPool.ref}`,
});
// In next code block
createCognitoIamRoles(this, identityPool.ref);
// Export values
new CfnOutput(this, "PinPointAppId", {
value: pinpointApp.ref,
});
new CfnOutput(this, "UserPoolId", {
value: userPool.userPoolId,
});
new CfnOutput(this, "UserPoolClientId", {
value: userPoolClient.userPoolClientId,
});
new CfnOutput(this, "IdentityPoolId", {
value: identityPool.ref,
});
This sets up all the resources except creating the needed IAM Roles and attaching them to the existing identity pool
import * as cdk from "@aws-cdk/core";
import * as iam from "@aws-cdk/aws-iam";
import * as cognito from "@aws-cdk/aws-cognito";
const cloudwatchPermissionPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["logs:PutLogEvents", "logs:CreateLogStream"],
resources: ["arn:aws:logs:*:*:log-group:*:log-stream:*"],
});
const pinpointPutEventsPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["mobiletargeting:PutEvents", "mobiletargeting:UpdateEndpoint"],
resources: ["arn:aws:mobiletargeting:*:*:apps/*"],
});
const getRole = (identityPoolRef: string, authed: boolean) => ({
assumedBy: new iam.FederatedPrincipal(
"cognito-identity.amazonaws.com",
{
StringEquals: {
"cognito-identity.amazonaws.com:aud": identityPoolRef,
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": authed
? "authenticated"
: "unauthenticated",
},
},
"sts:AssumeRoleWithWebIdentity"
),
});
export const createCognitoIamRoles = (
scope: cdk.Construct,
identityPoolRef: string
) => {
const authedRole = new iam.Role(
scope,
"CognitoAuthenticatedRole",
getRole(identityPoolRef, true)
);
const unAuthedRole = new iam.Role(
scope,
"CognitoUnAuthenticatedRole",
getRole(identityPoolRef, false)
);
authedRole.addToPolicy(cloudwatchPermissionPolicy);
authedRole.addToPolicy(pinpointPutEventsPolicy);
unAuthedRole.addToPolicy(cloudwatchPermissionPolicy);
unAuthedRole.addToPolicy(pinpointPutEventsPolicy);
new cognito.CfnIdentityPoolRoleAttachment(
scope,
"IdentityPoolRoleAttachment",
{
identityPoolId: identityPoolRef,
roles: {
authenticated: authedRole.roleArn,
unauthenticated: unAuthedRole.roleArn,
},
}
);
};
To create the resources, run npm run deploy
in the CDK repository. This will generate the needed resources and output some variable that will be needed in the next section.
Example output:
ClientSideLogTestCdkStack.IdentityPoolId = us-east-1:bc36bea5-5b0f-486a-8812-c68c2a5e4842
ClientSideLogTestCdkStack.PinPointAppId = a915587bb416449a8407fdd75bd6a0fe
ClientSideLogTestCdkStack.UserPoolClientId = 2sjihthbvodq1pos6m29mi6c2j
ClientSideLogTestCdkStack.UserPoolId = us-east-1_z4PrZ5N3Z
Client-Side Integration
Now that the needed infrastructure is ready we can start writing client code to interact with it.
To do that, let's create a Telemetry
class ( or whatever you would like to call it ) and use that as our entry-point to the provisioned AWS infrastructure.
This class should:
- Give access to Amplify's
Analytics
andAuth
libraries
The Amplify team have done the heavy lifting to provide user-friendly APIs, this implementation should attempt to leverage that work.
- Offer a simple abstraction over the CloudWatch client-logs API
The client doing the logging shouldn't care about CloudWatch APIs to be able to send logs. This telemetry client implementation provides three logging methods (info
, warn
and error
)
On instantiation, the object : - Retrieves credentials from Cognito - Creates a cloudwatch client - Instantiates Amplify's Auth and Analytics - Sets up a recurring timer to send collected logs to cloudwatch every 2 seconds.
You can find an example implementation here
Usage
You can find how the telemetry class is used by this react app.
import React from "react";
// client-side-telemetry-js = https://github.com/rakannimer/client-side-aws-telemetry/blob/master/client-side-telemetry-js/index.js
import AwsTelemetry from "client-side-telemetry-js";
// Config values are logged after you finish deployment with the CDK
const telemetryConfig = {
identityPoolId: "us-east-1:xxxxx-5b0f-486a-yzyz-c68c2a5ea2z2",
userPoolWebClientId: "2sjihyyyyyyypos6m29mi6c2j",
userPoolId: "us-east-1_z4PrZ5N3Z",
region: "us-east-1",
pinpointAppId: "d9ad53bad9d1qwe1w93d7de2499c7gf5",
};
const logger = new AwsTelemetry(telemetryConfig);
function App() {
React.useEffect(() => {
logger.info(Hello
);
setTimeout(() => {
logger.info(Hello 2 seconds later
);
}, 2200);
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<button
onClick={() => {
logger.warn("User clicked a button");
}}
>
Send a message to cloudwatch
</button>
</header>
</div>
);
}
export default App;
End Result
This should enable you to collect logs from your clients to identify and resolve problems before your customers have to report them.
Bonus Amazon Pinpoint Dashboard Screenshot :
Top comments (4)
@rakannimer How to protect identity pool ID?
Because, Identity pool id which store in frontend client (in mobile, web browser) easyly get it. Hacker can use it to attack by put logs to CloudWatch Logs spamming.
With authenticated user, they need login successfully => no problem
With unauthenticated user, they use identity pool id to have temporatory credentials and use it to put logs to CloudWatch.
However, Identity pool id store in Clientside that is not safe. Hacker can look for identity pool id and attack by put logs to CloudWatch.
Please, any solution?
Thank you!
@rakannimer I can't access the client code on GitHub.
Thanks for the heads up, fixed : github.com/rakannimer/client-side-...