Skip to content

Commit

Permalink
feat: add tracking
Browse files Browse the repository at this point in the history
This implements usage tracking of protocol and action
in Azure Monitor.

The feature is optional, must be configured to be enabled.
  • Loading branch information
coderbyheart committed Aug 5, 2024
1 parent a96c4af commit da1a4f3
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 673 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ Install the dependencies:
npm ci
```

### Configure Metrics (optional)

Configure the metrics for Azure Monitor logs ingestion (used for usage metrics
tracking):

```bash
aws ssm put-parameter --name /${STACK_NAME:-rest-echo}/metrics/endpoint --type String --value "<endpoint>"
aws ssm put-parameter --name /${STACK_NAME:-rest-echo}/metrics/dcrId --type String --value "<dcrId>"
aws ssm put-parameter --name /${STACK_NAME:-rest-echo}/metrics/streamName --type String --value "<streamName>"
aws ssm put-parameter --name /${STACK_NAME:-rest-echo}/metrics/secret --type SecureString --value "<secret>"
```

### Deploy

```bash
Expand Down
11 changes: 11 additions & 0 deletions cdk/resources/RESTAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
aws_dynamodb as DynamoDB,
aws_lambda as Lambda,
RemovalPolicy,
Stack,
aws_iam as IAM,
} from 'aws-cdk-lib'
import { RetentionDays } from 'aws-cdk-lib/aws-logs'
import { Construct } from 'constructs'
Expand Down Expand Up @@ -37,8 +39,17 @@ export class RESTAPI extends Construct {
environment: {
TABLE_NAME: storage.tableName,
NODE_NO_WARNINGS: '1',
STACK_NAME: Stack.of(this).stackName,
},
logRetention: RetentionDays.ONE_DAY,
initialPolicy: [
new IAM.PolicyStatement({
actions: ['ssm:GetParametersByPath'],
resources: [
`arn:aws:ssm:${Stack.of(this).region}:${Stack.of(this).account}:parameter/${Stack.of(this).stackName}/*`,
],
}),
],
})
storage.grantReadWriteData(lambda)
this.lambdaURL = lambda.addFunctionUrl({
Expand Down
92 changes: 92 additions & 0 deletions lambda/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
} = require('@aws-sdk/client-dynamodb')
const { unmarshall } = require('@aws-sdk/util-dynamodb')
const { randomUUID } = require('node:crypto')
const { GetParametersByPathCommand, SSMClient } = require('@aws-sdk/client-ssm')

const db = new DynamoDBClient({})
const TableName = process.env.TABLE_NAME
Expand All @@ -16,9 +17,99 @@ const cacheHeaders = {
'Cache-control': 'no-cache, no-store',
}

/**
* The endpoint URI uses the following format, where the Data Collection
* Endpoint and DCR Immutable ID identify the DCE and DCR.
* The immutable ID is generated for the DCR when it's created.
* You can retrieve it from the JSON view of the DCR in the Azure portal.
* Stream Name refers to the stream in the DCR that should handle the custom
* data.
*
* @see https://learn.microsoft.com/en-us/azure/azure-monitor/logs/logs-ingestion-api-overview#rest-api-call
*/
const tracker = ({ endpoint, dcrId, streamName, secret, debug }) => {
const url = new URL(
`${endpoint.toString().replaceAll(/\/$/g, '')}/dataCollectionRules/${dcrId}/streams/${streamName}?api-version=2023-01-01`,
)

debug?.('Tracking to', url.toString())

return async (
/**
* `https` or `http`
*/
protocol,
/**
* The REST method, e.g. `GET`, `POST`, `PUT`, or `DELETE`
*/
method,
) => {
const ts = new Date()
debug?.(
JSON.stringify([
{
TimeGenerated: ts.toISOString(),
Protocol: `REST:${protocol}`,
Action: method,
},
]),
)
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${secret}`,
},
body: JSON.stringify([
{
TimeGenerated: ts.toISOString(),
Protocol: `REST:${protocol}`,
Action: method,
},
]),
})
}
}

const settingsPromise = (async () => {
const ssm = new SSMClient({})
const Path = `/${process.env.STACK_NAME ?? 'rest-echo'}/metrics/`
const { Parameters } = await ssm.send(
new GetParametersByPathCommand({
Path,
WithDecryption: true,
}),
)
return (Parameters ?? []).reduce(
(acc, { Name, Value }) => ({ ...acc, [Name.replace(Path, '')]: Value }),
{},
)
})()

module.exports = {
handler: async (event) => {
console.log(JSON.stringify({ event }))

// Metrics tracking
let track
const { endpoint, dcrId, streamName, secret } = await settingsPromise
if (
endpoint !== undefined &&
dcrId !== undefined &&
streamName !== undefined &&
secret !== undefined
) {
track = tracker({
endpoint,
dcrId,
streamName,
secret,
debug: (...args) => console.debug('[Metrics]', ...args),
})
} else {
console.debug(`[Metrics]`, 'disabled')
}

const method = event.requestContext.http.method
const path = event.requestContext.http.path
const keySegment = path.split('/')[1]
Expand All @@ -32,6 +123,7 @@ module.exports = {
}

console.log(JSON.stringify({ method, key }))
await track?.(event.headers['x-forwarded-proto'], method)

if (method === 'POST' && key === 'new') {
return {
Expand Down
Loading

0 comments on commit da1a4f3

Please sign in to comment.