Skip to content

Commit

Permalink
Merge pull request #994 from aligent/feature/MICRO-53-notificaion-ser…
Browse files Browse the repository at this point in the history
…vice

MICRO-53: Add notification service generator
  • Loading branch information
tvhees authored Jul 7, 2024
2 parents 8ff5056 + 3829d17 commit 8801d62
Show file tree
Hide file tree
Showing 31 changed files with 2,619 additions and 151 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ Services are generated by our `@aligent/serverless-plugin`. It supports generati

#### Service generator (for generating new service)

`npx nx g service <service-name>`
- To generate a service, use the command:

```bash
npx nx g service <service-name>
# The command above is equivalent to 'npx nx g service <service-name> general'
```

- To generate a `notification` service, use the command:
```bash
npx nx g service <service-name> notification
```

#### Service executors

Our service executors are `lint`, `test`, `build`, `deploy` and `remove`. Executor can be executed using the command in the format:
Our service executors are `lint`, `test`, `check-types`, `build`, `deploy` and `remove`. Executor can be executed using the command in the format:

`npx nx run <service-name>:<executor> -- --<options>` or `npx nx <executor> <service-name> -- --<options>`

Expand All @@ -45,6 +55,7 @@ Libraries are generated by `@nx/js` plugin. For more information, check out thei
`npx nx g library <library-name>`

Shared library will need to have the `check-types` command added manually to ensure proper type checking. This is because the the `@nx/js` plugin does not add it by default.

```json
"check-types": {
"executor": "nx:run-commands",
Expand Down Expand Up @@ -86,5 +97,5 @@ Below are some example of general Nx. commands. For more information, check out
- [ ] Bespoke library generator -> use same base vite configuration if we do this.
- [ ] Develop workspace [preset](https://nx.dev/extending-nx/recipes/create-preset)
- [x] Pre-commit hooks
- [ ] Add error notification service
- [x] Add error notification service
- [ ] Add step function metric configuration
2,033 changes: 1,903 additions & 130 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"check-types:all": "nx run-many -t check-types",
"prepare": "[ -d .git ] && git config core.hooksPath '.git-hooks' || true"
},
"dependencies": {
"@aws-sdk/client-sns": "^3.606.0"
},
"devDependencies": {
"@aligent/serverless-conventions": "latest",
"@nx/devkit": "^17.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const YOUR_ENV_VAR = process.env.YOUR_ENV_VAR;
export const YOUR_ENV_VAR = process.env['YOUR_ENV_VAR'];

export const simpleObject = {
name: 'Test Object',
Expand Down
22 changes: 19 additions & 3 deletions tools/serverless-plugin/src/generators/service/generator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,31 @@ import { serviceGeneratorSchema } from './schema';

describe('service generator', () => {
let tree: Tree;
const options: serviceGeneratorSchema = { brand: 'test', name: 'test' };

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

it('should run successfully', async () => {
it('should run successfully when type is general', async () => {
const options: serviceGeneratorSchema = {
brand: 'test',
name: 'test',
type: 'general',
};
await serviceGenerator(tree, options);
const config = readProjectConfiguration(tree, 'test');
expect(config).toBeDefined();
expect(config.tags).toEqual(['service', 'general', 'test']);
});

it('should run successfully when type is notification', async () => {
const options: serviceGeneratorSchema = {
brand: 'test',
name: 'test',
type: 'notification',
};
await serviceGenerator(tree, options);
const config = readProjectConfiguration(tree, 'test');
expect(config).toBeDefined();
expect(config.tags).toEqual(['service', 'notification', 'test']);
});
});
37 changes: 24 additions & 13 deletions tools/serverless-plugin/src/generators/service/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import * as path from 'path';
import { serviceGeneratorSchema } from './schema';

const buildRunCommandConfig = (dir: string, command: string) => ({
const buildRunCommandConfig = (command: string, dir = '{projectRoot}') => ({
executor: 'nx:run-commands',
options: {
cwd: dir,
Expand All @@ -16,32 +16,39 @@ const buildRunCommandConfig = (dir: string, command: string) => ({
},
});

const getTemplateFilesLocation = (
type: serviceGeneratorSchema['type'] = 'general'
) => {
if (type === 'notification') {
return path.join(__dirname, 'notification-files');
}

return path.join(__dirname, 'general-files');
};

export async function serviceGenerator(
tree: Tree,
options: serviceGeneratorSchema
) {
const projectRoot = `services/${options.name}`;
addProjectConfiguration(tree, options.name, {
const { name, type } = options;
const projectRoot = `services/${name}`;

addProjectConfiguration(tree, name, {
root: projectRoot,
projectType: 'application',
sourceRoot: `${projectRoot}/src`,
targets: {
build: {
...buildRunCommandConfig(projectRoot, 'sls package'),
...buildRunCommandConfig('sls package'),
},
deploy: {
...buildRunCommandConfig(projectRoot, 'sls deploy'),
...buildRunCommandConfig('sls deploy'),
},
remove: {
...buildRunCommandConfig(projectRoot, 'sls remove'),
...buildRunCommandConfig('sls remove'),
},
'check-types': {
'executor': 'nx:run-commands',
'options': {
'cwd': '{projectRoot}',
'color': true,
'command': 'tsc --noEmit --pretty'
}
...buildRunCommandConfig('tsc --noEmit --pretty'),
},
lint: {
executor: '@nx/linter:eslint',
Expand All @@ -60,8 +67,12 @@ export async function serviceGenerator(
},
},
},
tags: ['service', type, name],
});
generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);

const templateFilesLocation = getTemplateFilesLocation(type);

generateFiles(tree, templateFilesLocation, projectRoot, options);
await formatFiles(tree);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# <%= name %>

This is a serverless notification system built using AWS Lambda and Amazon SNS. It allows you to send notifications to various channels such as email, Slack or even mobile push notification.

## Architecture

![Architecture Diagram](docs/architecture-diagram.svg)

The system consists of the following components:

1. AWS Lambda: A Lambda function is responsible for processing the notification requests and publishing messages to an SNS topic.
2. Amazon SNS: An SNS topic is used to distribute the notification messages to different channels (email, SMS, mobile push, etc.) based on subscriptions.
3. Subscribers: Various services or applications can subscribe to the SNS topic to receive notifications. For example, an email service can subscribe to receive email notifications, and `AWS Chatbot` service can subscribe to receive notifications and forward to Slack.

## Setup

- This service exports a Lambda Arn named as the endpoint for receiving notification. Other services can import it by: `!ImportValue: ErrorNotificationLambdaFunction-${self:provider.stage}`
- Since other services depends on this service exported value, we will need to tell Nx about this dependency by adding `implicitDependencies` & ``dependsOn` to the service `project.json` like so:

```json
{
"name": "service-name",
"implicitDependencies": ["notification"], // For `nx graph` only
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"cwd": "services/service-name",
"color": true,
"command": "sls package"
},
"dependsOn": [{ "projects": ["notification"], "target": "build", "params": "forward" }]
},
"deploy": {
"executor": "nx:run-commands",
"options": {
"cwd": "services/service-name",
"color": true,
"command": "sls deploy"
},
"dependsOn": [{ "projects": ["notification"], "target": "deploy", "params": "forward" }]
}
}
}
```

## Usage

This service accepts events in the following format (similar to CloudWatch Event events):

```json
{
"id": "00acdcb8-8864-405f-af3d-51a6bfb2d151",
"detail-type": "Lambda Execution Failed",
"source": "aws.lambda",
"time": "2024-06-24T04:42:33.884Z",
"region": "ap-southeast-2",
"resources": ["arn:aws:lambda:ap-southeast-2:XXXXXXXXXX:function:tt-int-shippit-order-dev-lambdaName"],
"detail": {
"executionArn": "arn:aws:lambda:ap-southeast-2:XXXXXXXXXX:function:tt-int-shippit-order-dev-lambdaName",
"logGroupName": "/aws/lambda/tt-int-shippit-order-dev-lambdaName",
"name": "tt-int-shippit-order-dev-lambdaName",
"status": "FAILED",
"error": "SyntaxError",
"cause": "{\"errorType\":\"SyntaxError\",\"errorMessage\":\"Unexpected token u in JSON at position 0\",\"trace\":\"SyntaxError: Unexpected token u in JSON at position 0\\n at JSON.parse (<anonymous>)\\n at Runtime.V1 (/src/lambda/create-shippit-order.ts:39:34)\\n at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)\"}"
}
}
```

The Lambda function will process the payload, format the notification message, and publish it to the SNS topic. The subscribed services will then receive the notification and handle it accordingly.

### Working with StepFunction

The `serverless-step-functions` plugin supports CloudWatch Event notifications. Therefore, we only need to add the following `notifications` configure:

```yaml
stepFunctions:
validate: true
stateMachines:
stateMachineName:
name: ${self:service}-${self:provider.stage}-stateMachineName
notifications:
ABORTED:
- lambda: arn:aws:lambda:${aws:region}:${aws:accountId}:function:tt-int-notification-${self:provider.stage}-notifyError
FAILED:
- lambda: arn:aws:lambda:${aws:region}:${aws:accountId}:function:tt-int-notification-${self:provider.stage}-notifyError
TIMED_OUT:
- lambda: arn:aws:lambda:${aws:region}:${aws:accountId}:function:tt-int-notification-${self:provider.stage}-notifyError
```
**_Note_**: It's unfortunate that `serverless-step-functions` does not support `!ImportValue`. Therefore, we have to manually build the notification arn.

### Working with Lambda

To send message to this notification service, we need to invoke the exported Lambda endpoint by [AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/lambda/command/InvokeCommand/).

For that reason, we need to update the `serverless.yml` file to include necessary permissions like so:

```yaml
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !ImportValue ErrorNotificationLambdaFunction-${self:provider.stage}
environment:
ERROR_NOTIFICATION_LAMBDA_ARN: !ImportValue ErrorNotificationLambdaFunction-${self:provider.stage}
```

Below is an example of how this service can be invoked:

```typescript
async notifyError(error: Error, context: Context) {
const payload = {
id: context.awsRequestId,
'detail-type': 'Lambda Execution Failed',
source: 'aws.lambda',
time: new Date().toISOString(),
region: context.invokedFunctionArn.split(':')[3] as string,
resources: [context.invokedFunctionArn],
detail: {
executionArn: context.invokedFunctionArn,
logGroupName: context.logGroupName,
name: context.functionName,
status: 'FAILED',
error: error.name,
cause: JSON.stringify({
errorType: error.name,
errorMessage: error.message,
trace: error.stack,
}),
},
};
const input: InvokeCommandInput = {
FunctionName: process.env.ERROR_NOTIFICATION_LAMBDA_ARN,
InvocationType: 'Event',
Payload: payload,
};
const response = await this.client.send(new InvokeCommand(input));
console.log('Error notification sent:', JSON.stringify(response));
return response;
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
flowchart LR
A[Other services] --> B(AWS Lambda)
B(AWS Lambda) --> C(Amazon SNS)
C --> D[Subscribers]
Loading

0 comments on commit 8801d62

Please sign in to comment.