Skip to content

Commit b4fc770

Browse files
committed
feat(grpc): implements the grpc parser
I'm not 100% sure of what data should be returned from the grpc parser, but this gets a basic implementation until we can get more feedback on it. fix #16
1 parent f8048c9 commit b4fc770

18 files changed

+637
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![CI](https://github.com/jmcdo29/ogma/workflows/CI/badge.svg) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Coffee](https://badgen.net/badge/Buy%20Me/A%20Coffee/purple?icon=kofi)](https://www.buymeacoffee.com/jmcdo29)
44

55
</div>
6-
# @ogma
6+
# Ogma
77

88
Ogma is a no-nonsense logger developed to make logging simple, and easy to read in development, while also having a powerful JSON form when it comes to production level logs, to make it easier to parse and consume by external services. This monorepo has all of the code for the base logger, the binary to rehydrate the JSON logs, and the [NestJS Module](https://nestjs.com) along with supported plugins for the module's interceptor.
99

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
2+
import { BaseExceptionFilter } from '@nestjs/core';
3+
4+
@Catch()
5+
export class ExceptionFilter extends BaseExceptionFilter {
6+
catch(exception: HttpException, host: ArgumentsHost) {
7+
const res = host.switchToHttp().getResponse();
8+
res.send(exception);
9+
}
10+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
Controller,
3+
Get,
4+
Inject,
5+
OnModuleInit,
6+
UseFilters,
7+
} from '@nestjs/common';
8+
import { ClientGrpc } from '@nestjs/microservices';
9+
import { ExceptionFilter } from './exception.filter';
10+
import { HelloService } from './hello-service.interface';
11+
12+
@Controller()
13+
export class GrpcClientController implements OnModuleInit {
14+
private helloService: HelloService;
15+
16+
constructor(@Inject('GRPC_SERVICE') private readonly grpc: ClientGrpc) {}
17+
18+
async onModuleInit() {
19+
this.helloService = this.grpc.getService<HelloService>('HelloService');
20+
}
21+
@Get()
22+
sayHello() {
23+
return this.helloService.sayHello({ ip: '127.0.0.1' });
24+
}
25+
26+
@Get('error')
27+
@UseFilters(ExceptionFilter)
28+
sayError() {
29+
return this.helloService.sayError({ ip: '127.0.0.1' });
30+
}
31+
32+
@Get('skip')
33+
saySkip() {
34+
return this.helloService.saySkip({ ip: '127.0.0.1' });
35+
}
36+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Module } from '@nestjs/common';
2+
import { ClientsModule, Transport } from '@nestjs/microservices';
3+
import { join } from 'path';
4+
import { GrpcClientController } from './grpc-client.controller';
5+
6+
@Module({
7+
imports: [
8+
ClientsModule.register([
9+
{
10+
transport: Transport.GRPC,
11+
name: 'GRPC_SERVICE',
12+
options: {
13+
package: 'hello',
14+
protoPath: join(__dirname, '..', 'hello/hello.proto'),
15+
},
16+
},
17+
]),
18+
],
19+
controllers: [GrpcClientController],
20+
})
21+
export class GrpcClientModule {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface HelloService {
2+
sayHello(data: { ip?: string }): { hello: string };
3+
sayError(data: { ip?: string }): { hello: string };
4+
saySkip(data: { ip?: string }): { hello: string };
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
syntax="proto3";
2+
3+
package hello;
4+
5+
service HelloService {
6+
rpc SayHello (Ip) returns (Greeting) {}
7+
8+
rpc SayError (Ip) returns (Greeting) {}
9+
10+
rpc SaySkip (Ip) returns (Greeting) {}
11+
}
12+
13+
message Greeting {
14+
string hello = 1;
15+
}
16+
17+
message Ip {
18+
optional string ip = 1;
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Catch, HttpException } from '@nestjs/common';
2+
import { BaseRpcExceptionFilter } from '@nestjs/microservices';
3+
import { Observable, throwError } from 'rxjs';
4+
5+
@Catch()
6+
export class ExceptionFilter extends BaseRpcExceptionFilter {
7+
catch(exception: HttpException): Observable<any> {
8+
return throwError(exception.message);
9+
}
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BadRequestException, Controller, UseFilters } from '@nestjs/common';
2+
import { GrpcMethod } from '@nestjs/microservices';
3+
import { OgmaSkip } from '@ogma/nestjs-module';
4+
import { AppService } from '../../app.service';
5+
import { ExceptionFilter } from './exception.filter';
6+
7+
@Controller()
8+
export class GrpcServerController {
9+
constructor(private readonly service: AppService) {}
10+
11+
@GrpcMethod('HelloService', 'SayHello')
12+
sayHello() {
13+
return this.service.getHello();
14+
}
15+
16+
@GrpcMethod('HelloService', 'SayError')
17+
@UseFilters(ExceptionFilter)
18+
sayError() {
19+
throw new BadRequestException('Borked');
20+
}
21+
22+
@OgmaSkip()
23+
@GrpcMethod('HelloService', 'SaySkip')
24+
saySkip() {
25+
return this.service.getHello();
26+
}
27+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
import { AppService } from '../../app.service';
3+
import { GrpcServerController } from './grpc-server.controller';
4+
5+
@Module({
6+
controllers: [GrpcServerController],
7+
providers: [AppService],
8+
})
9+
export class GrpcServerModule {}

integration/test/grpc.spec.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,94 @@
1-
describe('gRPC test', () => {
2-
it.todo('implement test');
1+
import { INestApplication, INestMicroservice } from '@nestjs/common';
2+
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
3+
import { Test } from '@nestjs/testing';
4+
import { color } from '@ogma/logger';
5+
import { GrpcParser } from '@ogma/platform-grpc';
6+
import { OgmaInterceptor } from '@ogma/nestjs-module';
7+
import { join } from 'path';
8+
import { GrpcServerModule } from '../src/grpc/server/grpc-server.module';
9+
import { GrpcClientModule } from '../src/grpc/client/grpc-client.module';
10+
import {
11+
createTestModule,
12+
getInterceptor,
13+
hello,
14+
httpPromise,
15+
serviceOptionsFactory,
16+
} from './utils';
17+
18+
describe('GrpcParser', () => {
19+
let rpcServer: INestMicroservice;
20+
let rpcClient: INestApplication;
21+
let interceptor: OgmaInterceptor;
22+
beforeAll(async () => {
23+
const modRef = await createTestModule(GrpcServerModule, {
24+
service: serviceOptionsFactory('gRPC Server'),
25+
interceptor: {
26+
rpc: GrpcParser,
27+
},
28+
});
29+
rpcServer = modRef.createNestMicroservice<MicroserviceOptions>({
30+
transport: Transport.GRPC,
31+
options: {
32+
protoPath: join(__dirname, '..', 'src', 'grpc', 'hello/hello.proto'),
33+
package: 'hello',
34+
},
35+
});
36+
interceptor = getInterceptor(rpcServer);
37+
await rpcServer.listenAsync();
38+
const clientRef = await Test.createTestingModule({
39+
imports: [GrpcClientModule],
40+
}).compile();
41+
rpcClient = clientRef.createNestApplication();
42+
await rpcClient.listen(0);
43+
});
44+
45+
afterAll(async () => {
46+
await rpcClient.close();
47+
await rpcServer.close();
48+
});
49+
50+
describe('server calls', () => {
51+
let logSpy: jest.SpyInstance;
52+
let baseUrl: string;
53+
54+
beforeAll(async () => {
55+
baseUrl = await rpcClient.getUrl();
56+
});
57+
58+
beforeEach(() => {
59+
logSpy = jest.spyOn(interceptor, 'log');
60+
});
61+
62+
afterEach(() => {
63+
logSpy.mockClear();
64+
});
65+
66+
it.each`
67+
url | status | endpoint
68+
${'/'} | ${color.green(200)} | ${'SayHello'}
69+
${'/error'} | ${color.red(500)} | ${'SayError'}
70+
`(
71+
'$url call',
72+
async ({
73+
url,
74+
status,
75+
endpoint,
76+
}: {
77+
url: string;
78+
status: string;
79+
endpoint: string;
80+
}) => {
81+
await httpPromise(baseUrl + url);
82+
expect(logSpy).toBeCalledTimes(1);
83+
const logObject = logSpy.mock.calls[0][0];
84+
expect(logObject).toBeALogObject('gRPC', endpoint, 'grpc', status);
85+
},
86+
);
87+
88+
it('should call the skip but not log it', async () => {
89+
const data = await httpPromise(baseUrl + '/skip');
90+
expect(logSpy).toHaveBeenCalledTimes(0);
91+
expect(data).toEqual(hello);
92+
});
93+
});
394
});

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@commitlint/cli": "^8.3.5",
5454
"@commitlint/config-conventional": "^8.3.4",
5555
"@golevelup/ts-jest": "^0.2.1",
56+
"@grpc/proto-loader": "^0.5.4",
5657
"@nestjs/cli": "^7.0.2",
5758
"@nestjs/common": "^7.0.0",
5859
"@nestjs/core": "^7.0.0",
@@ -90,6 +91,7 @@
9091
"fastify": "^2.13.0",
9192
"graphql": "^14.0.0",
9293
"graphql-tools": "^4.0.0",
94+
"grpc": "^1.24.2",
9395
"husky": "^4.2.3",
9496
"jest": "^25.1.0",
9597
"lerna": "^3.20.2",

packages/platform-grpc/README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
# `@ogma/platform-grpc`
22

3-
> TODO: description
3+
The `GrpcParser` parser for the `OgmaInterceptor`. This plugin class parses Kafka request and response object to be able to successfully log the data about the request. For more information, check out [the @ogma/nestjs-module](../nestjs-module/README.md) documentation.
4+
5+
## Installation
6+
7+
Nothing special, standard `npm i @ogma/platform-grpc` or `yarn add @ogma/platform-grpc`
48

59
## Usage
610

7-
```
8-
const platformGrpc = require('@ogma/platform-grpc');
11+
This plugin is to be used in the `OgmaInterceptorOptions` portion of the `OgmaModule` during `forRoot` or `forRootAsync` registration. It can be used like so:
912

10-
// TODO: DEMONSTRATE API
13+
```ts
14+
@Module(
15+
OgmaModule.forRoot({
16+
interceptor: {
17+
rpc: GrpcParser
18+
})
19+
)
20+
export class AppModule {}
1121
```
22+
23+
## Important Notes
24+
25+
Because of how gRP requests are sent and the data available in them, to get the IP address in the request log, an IP property must be sent in the payload. This is the only way to get the IP address. If an IP property is not sent, the interceptor will use an empty string. [You can find more information here](https://stackoverflow.com/questions/45235080/how-to-know-the-ip-address-of-mqtt-client-in-node-js).

packages/platform-grpc/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
"url": "git+https://github.com/jmcdo29/ogma.git"
2828
},
2929
"scripts": {
30-
"test": "echo \"No tests to run\""
30+
"prebuild": "rimraf lib",
31+
"build": "tsc -p tsconfig.build.json",
32+
"postbuild": "mv ./lib/src/* ./lib && rmdir lib/src",
33+
"test": "jest",
34+
"test:cov": "jest --coverage"
3135
},
3236
"bugs": {
3337
"url": "https://github.com/jmcdo29/ogma/issues"
@@ -39,6 +43,8 @@
3943
"rxjs": "^6.5.4"
4044
},
4145
"peerDependencies": {
42-
"@ogma/nestjs-module": "^0.1.0"
46+
"@ogma/nestjs-module": "^0.1.0",
47+
"@nestjs/microservices": "^7.0.0",
48+
"grpc": "^1.24.0"
4349
}
4450
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ExecutionContext, Injectable } from '@nestjs/common';
2+
import { PATTERN_METADATA } from '@nestjs/microservices/constants';
3+
import { AbstractInterceptorService } from '@ogma/nestjs-module';
4+
5+
@Injectable()
6+
export class GrpcParser extends AbstractInterceptorService {
7+
getCallPoint(context: ExecutionContext) {
8+
return this.reflector.get(PATTERN_METADATA, context.getHandler()).rpc;
9+
}
10+
11+
getCallerIp(context: ExecutionContext) {
12+
const data = this.getData(context);
13+
return data?.ip || '';
14+
}
15+
16+
getProtocol() {
17+
return 'grpc';
18+
}
19+
20+
getMethod() {
21+
return 'gRPC';
22+
}
23+
24+
getStatus(
25+
context: ExecutionContext,
26+
inColor: boolean,
27+
error?: Error | ExecutionContext,
28+
): string {
29+
const status = error ? 500 : 200;
30+
return inColor ? this.wrapInColor(status) : status.toString();
31+
}
32+
33+
private getData(context: ExecutionContext) {
34+
return context.switchToRpc().getData();
35+
}
36+
}

packages/platform-grpc/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './grpc-interceptor.service';

0 commit comments

Comments
 (0)