NESTJSと回路図によるマイクロサービスの生成


MicroServiceとNESTJSについてのもう一つのスーパーポストへようこそ!私たちはしばらくの間このプロジェクトに取り組んでいます、そして、私はこの方法を使用する方法と私たちがテンプレートプロジェクトを構築した理由をあなたと共有することに決めました.…始めましょう!
テンプレートプロジェクトジェネレータパッケージはgithub

論理学とは何か
あなたが何がschematicsであるかについて、わからないならば、私は彼らが何であるかについて簡単な概要をします.しかし、私はあなたが見てみることをお勧めofficial documentation .
それを合計するために、図式はいくつかの種類の青写真ですが、コードのために.Yoはあなたが生成したいファイルと特別なテンプレート言語で定義し、最終的なファイルを生成するためにそれを実行します.
そして何がNESTJSとはこれを行うのですか?涼しい事はNest CLI フードの下に角の仕組みを使用し、さらに良い、彼らはリファレンスを参照してカスタムコレクションを参照することができます!
それはあなたのカスタム脚本を作成することができますまだ、巣のCLI、クールな右からすべての機能を得ることを意味?

なぜプロジェクトジェネレータ?
反乱の我々は、Netflixが彼らの技術協議の1つで使用される概念の多くを信じます.
舗装道路は、(私の観点から)開発し、繰り返しタスクをスピードアップするために必要なツールを持っていることを意味します.
たとえば、毎回マイクロサービス用の新しいプロジェクトを作成する必要があるときは、デフォルトのNSTEJSプロジェクトを生成し、マイクロサービスパッケージを追加し、NATSパッケージを追加し、持続性レイヤーを追加し、いくつかのグローバルインターセプターを作成します.タスクのこれらのセットは常に同じであり、簡単な方法で自動化することができます.そして、その方法は、回路図を備えた発電機を使用しています.

使い方は?
プロジェクトを簡単に使用すると、単にグローバルNPMパッケージとしてインストールする必要がありますネストのCLIで参照します.
npm install -g @rebellionpay/nest-ms-template

# for generating an app
nest g -c @rebellionpay/nest-ms-template app

# for generating a controller
nest g -c @rebellionpay/nest-ms-template ctrl
次に、あなただけの質問に答えて、ファイルが生成される予定です.

このテンプレートは何ですか?
これは本当に重要です
このプロジェクトは、反逆賃金のための舗装道路です.これは、我々の会社と我々のチームのニーズに合うものが含まれる機能を意味します.
それは、私はプロジェクトの生成されたファイルをご案内しましょう.
質問に答えを与えてください.
? What name would you like to use for the new project? sample-project
? Who is the author of the project? Rebellion Pay <[email protected]>
? Which is the description of the project? This is a sample project
? Which is the project license? MIT
? In which port will it run? 3000
? Which transport layer would you like to use? NATS
? Are you building a pure app? No
? Are you going to use persistence in a DB? Yes
? Which database would you like to use? mongodb
? Are you going to make the CD with spinnaker? Yes
? What is the spinnaker API url? https://testing.spinnaker.com
? In which kubernetes namespace will this app going to be deployed? default
その答えに対して、次のファイルが生成されます.
.
├── .vscode
│   └── launch.json
├── kubernetes
│   └── manifest.yml
├── src
│   ├── config
│   │   ├── MongoConfigService.ts
│   │   └── NATSConfigService.ts
│   ├── factory
│   │   └── winstonConfig.ts
│   ├── filters
│   │   └── ExceptionsFilter.ts
│   ├── interceptors
│   │   ├── InjectMetadataInterceptor.ts
│   │   ├── MetricsInterceptor.ts
│   │   └── TimeoutInterceptor.ts
│   ├── interface
│   │   └── MicroserviceMessage.ts
│   ├── message
│   │   ├── message.module.ts
│   │   ├── message.service.spec.ts
│   │   └── message.service.ts
│   ├── metrics
│   │   ├── metrics.module.ts
│   │   └── metrics.service.ts
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .gitlab-ci.yml
├── .prettierrc
├── README.md
├── nest-cli.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json
ルートフォルダでは、標準のREADMEファイル、typescript configファイル、パッケージがあります.JSONと巣CLI.MonorPeoプロジェクトに使用されるJSON.サンプルもあります.envファイルとeslint/prettier設定ファイル.そして最後に、gitignoreとgitlab ciファイル.
また、ある.vscodekubernetes フォルダ..フォルダにはlaunch.json NestJSプロジェクトの標準的な設定で.Kubernetesフォルダには、サービスと展開を持つマニフェストファイルが含まれます.

We are going to use block quotes to explain the differences in the code if you choose other alternatives to the questions.



メイン.TS
srcフォルダにmain.ts ファイル.ネストアプリケーションを起動する機能があります.我々の答えの場合main.ts ブートストラップhybrid app .
if (process.env.NODE_ENV === 'production') {
  initializeAPMAgent({
    serverUrl: process.env.ELASTIC_APM_SERVER_URL,
    serviceName: process.env.ELASTIC_APM_SERVICE_NAME,
    secretToken: process.env.ELASTIC_APM_SECRET_TOKEN,
  });
}

async function bootstrap() {
  const logger: LoggerConfig = new LoggerConfig();
  const winstonLogger = WinstonModule.createLogger(logger.console());

  const app = await NestFactory.create(
    AppModule.register(),
    {
      cors: true,
      logger: WinstonLogger
    }
  );

  const natsConfigService : NATSConfigService = app.get(NATSConfigService);
  const configService : ConfigService = app.get<ConfigService>(ConfigService);

  app.use(helmet());
  app.useGlobalFilters(new AllExceptionsFilter());


  app.connectMicroservice({
    ...natsConfigService.getNATSConfig
  });

  const globalInterceptors = [];

  if (process.env.NODE_ENV === 'production') {
    globalInterceptors.push(
      app.get(ApmHttpUserContextInterceptor),
      app.get(ApmErrorInterceptor)
    );
    const apmMiddleware = app.get(APM_MIDDLEWARE);
    app.use(apmMiddleware);
  }

  globalInterceptors.push(
    new TimeoutInterceptor()
  );
  app.useGlobalInterceptors(... globalInterceptors);

  const port = configService.get<number>('PORT') || 3000;
  app.startAllMicroservicesAsync();

  await app.listen(port, () => winstonLogger.log(`Hybrid sample-project test running on port ${port}`));

}
bootstrap();
ご覧のように、APMエージェントをAPMに送信する設定を開始します.これは、サービスのメトリクスを集めるためのツールの一つです.
また、ログインのためにWinstonを使用します.ですから、最初にロガーをインスタンス化し、ネスト工場に渡します.
次にグローバルインターセプターとグローバルフィルタを追加します.我々が生産中であるならば、我々はapmをセットアップして、迎撃機を使うことを終えます.
我々が使用しているフィルタは、アプリケーションで発生するすべての例外をキャッチするためです.
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest();
    const response = ctx.getResponse();

    let status = 500;

    if(exception instanceof HttpException) {
      status = exception.getStatus();
    }

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
フィルタが例外をキャッチし、標準応答を返すことがわかります.
一方、タイムアウトインターセプターは送信されたすべてのメッセージに対してだけ動作し、解決するために5秒以上かかると例外をスローします.

If you choose a pure app, the code is almost the same. But we don't have to define any port and the metadata interceptor can be used also as a global one.



アプリ*.TS
アプリ*.TSファイルにはメインモジュールコードが含まれます.

アプリ.モジュールです.TS
@Module({})
export class AppModule {
  public static register(): DynamicModule {
    const imports = [
      MongooseModule.forRootAsync({
        useClass: MongoConfigService,
      }),
      WinstonModule.forRoot(logger.console()),
      ConfigModule.forRoot({
        isGlobal: true,
        validationSchema: JoiObject({
          NODE_ENV: JoiString()
            .valid('development', 'production', 'test')
            .default('development'),
          PORT: JoiNumber().port().default(3030),
          ELASTIC_APM_SERVER_URL: JoiString(),
          ELASTIC_APM_SERVICE_NAME: JoiString(),
          ELASTIC_APM_SECRET_TOKEN: JoiString(),
          NATS_URL: JoiString().required().default('nats://localhost:4222'),
          NATS_USER: JoiString(),
          NATS_PASSWORD: JoiString(),
          MONGODB_URI: JoiString().uri().required(),
          INFLUX_URL: JoiString().uri()
        })
      }),
      MetricsModule,
      MessageModule
    ];

    if(process.env.NODE_ENV === 'production') {
      imports.push(ApmModule.forRootAsync({
        useFactory: async () => {
          return {
            httpUserMapFunction: (req: any) => {
              return {
                ...req
              };
            },
          };
        },
      }));
    }

    const controllers = [AppController];

    const providers = [NATSConfigService, AppService];

    return {
      module: AppModule,
      imports,
      controllers,
      providers,
    };
  }
}
ご覧の通り、動的モジュールを使用しています.APMモジュールを生産する際にのみ登録することができます.
JOIを使用して、. NET Frameworkに存在する設定を検証します.envファイル.
この場合、私たちは持続性のためにmongooseを使用していますが、また、MySQLとPostgreSQLを選ぶためのオプションがあります.
また、メトリックとメッセージモジュールの2つのモジュールをインポートします.

If you choose a pure app the metrics interceptor is going to be used also as a provider.

If you use a different persistence layer you will see there another different config service injected there. If no persistence chosen, no config service injected.



メトリック.モジュールです.TS
メトリックのモジュールは単純であり、メトリクスインターセプターで使用されます.インターセプターは、ちょうどすべての要求(HTTPまたはRPC)の真ん中に置かれて、我々のメトリック基盤にいくつかのデータを送るメトリクスサービスを呼び出します.
@Injectable()
export class MetricsService {
  constructor( private configService: ConfigService) { }

  private influx = new Influx.InfluxDB(this.configService.get<string>('INFLUX_URL') || 'http://localhost:8086/telegraf');

  async send(measurement: string, fields: Record<string, any>): Promise<void> {
    this.influx.writePoints([{
      measurement,
      fields
    }]);
  }
}
それは、XiXStreffDBにメトリクスを送信するためのサービスです.
メッセージモジュールを持っています.このモジュールはNESTJSと同じ機能を実装するメッセージサービスをインポートするだけですClientProxy .
@Injectable()
export class MessageService {
  constructor(@Inject('MESSAGE_CLIENT') private client: ClientProxy){};

  sendMessage<Toutput = any, Tinput = any>(pattern: Record<string, any> | string, message: MicroserviceMessage<Tinput>): Observable<Toutput> {
    return this.client.send<Toutput, MicroserviceMessage<Tinput>>(pattern, message);
  }

  emitMessage<Toutput = any, Tinput = any>(pattern: Record<string, any> | string, message: MicroserviceMessage<Tinput>): Observable<Toutput> {
    return this.client.emit<Toutput, MicroserviceMessage<Tinput>>(pattern, message);
  }
}
我々はラッピング機能を使用するので、同じ場所にメッセージの送信を集中化したい場合は、いくつかの時点で、我々はより多くのメトリック、コントロール、またはメッセージの送信で何を実装する必要がある場合、我々はただ1つの場所に配置する必要があります.
また、私たち自身のMicroServiceMessageインタフェースを定義します.これは、パターンとペイロードの他に、ログに使用するメタデータです.

インターセプター
プロジェクトでは3つのインターセプターがあります.メトリック、メタデータとタイムアウトインターセプターの1つ.タイムアウトは既に説明されている.
メタデータインターセプターは別の簡単なインターセプターです.すべてのRPCメッセージを受け取り、メッセージのパターンを取得します.これを使用して、各リクエストに対して、ログラインをトリガーしたメッセージパターンをメッセージのメタデータに印刷できます.これにより、ユーザーフロー内で送信されるメッセージパターンごとに特定のリクエストIDをトレースできます.
メトリクスインターセプターは、以前に見たメトリックサービスを使用してメトリクスを送るだけのインターセプターです.それはちょうど要求ごとに行動して、リクエストタイプ(HTTPまたはRPC)に応じて異なるメトリックを集めます.最後に、メトリクスサービスを使用して流入にこれらのメトリックを送信します.

サービスとコントローラ
また、生成するapp.service.tsapp.controller.ts それぞれの必要な設定を説明します.
@UseInterceptors(MetricsInterceptor, InjectMetadataInterceptor)
@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly messageService: MessageService,
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) { }

  @MessagePattern({ cmd: 'YOUR_CMD' })
  yourMessageHandler(@Payload() message: MicroserviceMessage): Record<string, unknown> {
    this.logger.info(message.data, message.metadata);
    return { data: message.data, metadata: message.metadata };
  }

  @Post()
  async yourPostHandler(
    @Body() body: Record<string, any>,
    @Headers('x-request-id') reqId: string
  ): Promise<unknown> {
    this.logger.info({ body, reqId });
    const response = await this.messageService.sendMessage({ cmd: 'YOUR_CMD' }, { data: body, metadata: { reqId } });
    return (await response.toPromise()).data;
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/status')
  getStatus(): string {
    return '[OK]';
  }
}
ここでの主なものは使用されるインターセプターです.メトリックやメタデータのインターセプターは、ハイブリッドアプリケーションでは、グローバルに使用する場合、HTTPリクエストだけでトリガされる予定ですので、ここで注入する必要があります.
他のすべての関数は、標準のコントローラ関数です.
注釈を使います@Headers('x-request-id') reqId: string 私たちはゲートウェイとして香港を使用して、それぞれの要求にユニークなIDを割り当てるために設定されたプラグインを持っているので.

If you use a pure app the interceptors are not going to be put there because the can be used globally. Also you will not have the HTTP endpoints.



ボーナス・トラック
私が言ったように、純粋なアプリケーションやハイブリッドのいずれかを使用している場合は、コントローラが異なります.そのため、テンプレートプロジェクトもコントローラの生成をサポートしています.質問は本当にシンプルであり、純粋なアプリケーションコントローラまたはハイブリッドアプリケーションの間の唯一の違いは、傍受です.
あなたが純粋なアプリを使用する場合は、注入されませんし、しない場合は、彼らはされます.

結論
ポストの冒頭で言ったように、これは我が社の舗装道路です.私は、これはクールなプロジェクトであり、誰か他のプロジェクトの出発点として機能するか、誰もが自分のマイクロサービスプロジェクトをブートするのに役立つことができると思います.
あなたが私に脚本の技術的な部分と私がこのテンプレートプロジェクトを構築した方法について話している別のポストを書くことを望むならば、コメントしてください.
また、無料で問題を埋めるために感じるproject repo あなたが何かを逃す場合でも、プル要求を開きます.
次のポストでお会いしましょう!