├── .commitlintrc.json ├── .editorconfig ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── eslint.config.mjs ├── eslint.config.spec.mjs ├── gulpfile.js ├── lerna.json ├── package.json ├── packages ├── nestjs-config │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── config.constants.ts │ │ ├── config.module.ts │ │ ├── config.service.ts │ │ ├── index.ts │ │ └── interfaces │ │ │ ├── config-module-options.interface.ts │ │ │ └── index.ts │ ├── package.json │ ├── tests │ │ ├── e2e │ │ │ ├── load-env-file.spec.ts │ │ │ ├── load-env-mutiple-file.spec.ts │ │ │ ├── process-env.spec.ts │ │ │ ├── valid-schema.spec.ts │ │ │ └── validation.yaml │ │ ├── jest-e2e.json │ │ └── src │ │ │ ├── app.module.ts │ │ │ └── config │ │ │ ├── development-valid.yaml │ │ │ ├── development.yaml │ │ │ └── development2.yaml │ └── tsconfig.json ├── nestjs-logger │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── constant.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── logger-interface.ts │ │ ├── logger.module.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── json-logger.service.ts │ │ │ └── plain-logger.service.ts │ │ └── util │ │ │ └── common.util.ts │ ├── package.json │ ├── test │ │ ├── async-module-json-logger.service.spec.ts │ │ ├── config │ │ │ ├── config.module.ts │ │ │ └── config.service.ts │ │ ├── jest-e2e.json │ │ ├── json-logger.service.spec.ts │ │ ├── plain-logger.service.spec.ts │ │ └── util │ │ │ └── common.util.spec.ts │ └── tsconfig.json ├── nestjs-prom │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── constant.ts │ │ ├── decorator │ │ │ ├── index.ts │ │ │ ├── prom.decorator.ts │ │ │ └── prom.paramter.decorator.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ ├── metric.type.ts │ │ │ ├── prom-module-async-options.interface.ts │ │ │ └── prom-options.interface.ts │ │ ├── prom-metric.provider.ts │ │ └── prom.module.ts │ ├── package.json │ ├── test │ │ ├── jest-e2e.json │ │ ├── prom.async.module.spec.ts │ │ └── prom.module.spec.ts │ └── tsconfig.json ├── nestjs-tracing │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── aop │ │ │ ├── index.ts │ │ │ ├── tracing-grpc.interceptor.ts │ │ │ └── tracing-http.interceptor.ts │ │ ├── constant.ts │ │ ├── decorator │ │ │ └── spand.decorator.ts │ │ ├── hook │ │ │ ├── async-context.ts │ │ │ ├── async-hooks-helper.ts │ │ │ ├── async-hooks-interceptor.ts │ │ │ ├── async-hooks-middleware.ts │ │ │ ├── async-hooks-module.ts │ │ │ ├── async-hooks-storage.ts │ │ │ └── index.ts │ │ ├── http-service.interceptors.ts │ │ ├── index.ts │ │ ├── interface │ │ │ └── tracing.interface.ts │ │ ├── tracing.module.ts │ │ └── util │ │ │ ├── span.util.ts │ │ │ └── tracing-module-options.util.ts │ ├── package.json │ ├── test │ │ ├── hook.module.spec.ts │ │ ├── jest-e2e.json │ │ ├── test2.spec.ts │ │ └── tracing-module.spec.ts │ └── tsconfig.json └── tsconfig.base.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json5 ├── sample ├── async-hook │ ├── package.json │ └── src │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── grpc.controller.ts │ │ ├── main.ts │ │ ├── proto │ │ ├── raw.proto │ │ └── raw.service.ts │ │ └── service │ │ ├── db.service.ts │ │ └── grpc.service.ts └── hooks │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ ├── package.json │ └── service │ ├── db.service.ts │ ├── grpc.service.ts │ └── index.ts ├── tools └── gulp │ ├── config.ts │ ├── gulpfile.ts │ ├── tasks │ ├── clean.ts │ ├── copy-misc.ts │ ├── packages.ts │ └── samples.ts │ ├── tsconfig.json │ └── util │ └── task-helpers.ts ├── tsconfig.json └── tsconfig.spec.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 17 * * 4' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v2 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v2 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # bundle 10 | packages/**/*.d.ts 11 | packages/**/*.js 12 | 13 | # misc 14 | .DS_Store 15 | lerna-debug.log 16 | npm-debug.log 17 | yarn-error.log 18 | /**/npm-debug.log 19 | /packages/**/.npmignore 20 | /packages/**/LICENSE 21 | *.tsbuildinfo 22 | 23 | # example 24 | /quick-start 25 | /example_dist 26 | /example 27 | 28 | # tests 29 | /test 30 | /benchmarks/memory 31 | /coverage 32 | /.nyc_output 33 | /packages/graphql 34 | /benchmarks/memory 35 | build/config\.gypi 36 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --config .commitlintrc.json --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | **/*.ts 3 | *.ts 4 | 5 | # definitions 6 | !**/*.d.ts 7 | !*.d.ts 8 | 9 | # configuration 10 | package-lock.json 11 | tslint.json 12 | tsconfig.json 13 | .prettierrc 14 | 15 | *.tsbuildinfo 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/**/*.d.ts 2 | packages/**/*.js 3 | 4 | pnpm-lock.yaml 5 | 6 | /.nx/workspace-data -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2019 Kamil Myśliwiec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # donews 2 | 3 | ![npm](https://img.shields.io/npm/v/@donews/nestjs-config?logo=Npm%20Version%20) 4 | ![license](https://img.shields.io/github/license/DoNewsCode/nestjs) 5 | 6 | ## Note 7 | 8 | 如果 nest 升级 v8.0.0,请更新 0.9.0 9 | 10 | ## 描述 11 | 12 | donews nest 相关组件项目,采用 lerna 项目结构布局,减少项目数量,防止修改 lint 规则,等公共基础服务时,需要更新太多的项目。 13 | 14 | 目前包含组件 15 | 16 | 1. [config](./packages/nestjs-config#readme) 17 | 2. [logger](./packages/nestjs-logger#readme) 18 | 3. [prom](./packages/nestjs-prom#readme) 19 | 4. [tracing](./packages/nestjs-tracing#readme) 20 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import importlint from 'eslint-plugin-import'; 3 | import prettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default [ 8 | js.configs.recommended, 9 | importlint.flatConfigs.errors, 10 | importlint.flatConfigs.warnings, 11 | importlint.flatConfigs.typescript, 12 | ...tseslint.configs.recommended, 13 | prettierRecommended, 14 | 15 | { 16 | name: 'custom-language-option', 17 | languageOptions: { 18 | globals: { 19 | ...globals.node, 20 | ...globals.jest, 21 | }, 22 | ecmaVersion: 5, 23 | sourceType: 'module', 24 | parserOptions: { 25 | project: 'tsconfig.json', 26 | }, 27 | }, 28 | }, 29 | { 30 | name: 'custom-rule', 31 | rules: { 32 | '@typescript-eslint/no-empty-object-type': 'warn', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/no-require-imports': 'warn', 35 | '@typescript-eslint/no-unused-vars': 'off', 36 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 37 | 38 | 'no-console': 'off', 39 | 40 | // eslint-plugin-import 41 | 'import/order': [ 42 | 'error', 43 | { 'newlines-between': 'always', alphabetize: { order: 'asc' } }, 44 | ], 45 | 'import/no-unresolved': ['off', { ignore: ['.*\\.js$'] }], 46 | 47 | //ECMAScript 6 48 | 'prefer-const': 'error', 49 | 'sort-imports': [ 50 | 'error', 51 | { ignoreDeclarationSort: true, ignoreCase: true }, 52 | ], 53 | }, 54 | }, 55 | { 56 | name: 'global-ignore', 57 | ignores: [ 58 | '**/.idea', 59 | '**/*.iml', 60 | '**/out', 61 | '**/gen', 62 | '**/dist', 63 | '**/node_modules/**', 64 | ], 65 | }, 66 | ]; 67 | -------------------------------------------------------------------------------- /eslint.config.spec.mjs: -------------------------------------------------------------------------------- 1 | import rootLint from './eslint.config.mjs'; 2 | 3 | export default [ 4 | ...rootLint, 5 | { 6 | name: 'custom-spec-language-option', 7 | languageOptions: { 8 | parserOptions: { 9 | project: 'tsconfig.spec.json', 10 | sourceType: 'module', 11 | }, 12 | }, 13 | }, 14 | { 15 | name: 'custom-spec-rule', 16 | rules: { 17 | '@typescript-eslint/no-empty-function': 'off', 18 | }, 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all 4 | * the tasks. The tasks are really inside tools/gulp/tasks. 5 | */ 6 | 7 | const path = require('path'); 8 | 9 | const projectDir = __dirname; 10 | const tsconfigPath = path.join(projectDir, 'tools/gulp/tsconfig.json'); 11 | 12 | require('ts-node').register({ 13 | project: tsconfigPath 14 | }); 15 | 16 | require('./tools/gulp/gulpfile'); -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.11.1", 4 | "packages": ["packages/*"], 5 | "npmClient": "pnpm" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donews", 3 | "author": "toonew", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "husky", 7 | "build": "gulp build", 8 | "prebuild:prod": "npm run clean", 9 | "build:prod": "gulp build", 10 | "build:samples": "npm run build:prod && gulp test:samples && gulp test:e2e:samples", 11 | "clean": "gulp clean:bundle", 12 | "format": "prettier \"**/*.ts\" --ignore-path ./.prettierignore --write && git status", 13 | "test": "nyc mocha packages/**/*.spec.ts --reporter spec", 14 | "lint": "concurrently \"npm run lint:packages\" \"npm run lint:spec\"", 15 | "lint:fix": "concurrently \"npm run lint:packages -- --fix\" \"npm run lint:spec -- --fix\"", 16 | "lint:packages": "eslint \"packages/**/**.ts\" --ignore-pattern \"packages/**/*.spec.ts\"", 17 | "lint:spec": "eslint \"packages/**/**.spec.ts\" -c \"eslint.config.spec.mjs\"", 18 | "prerelease": "gulp copy-misc", 19 | "publish": " npm run prerelease && npm run build:prod && npx lerna publish --exact --force-publish -m \"chore(@donews): publish %s release\"", 20 | "publish:beta": "npm run prerelease && npm run build:prod && npx lerna publish --dist-tag=beta -m \"chore(@donews): publish %s release\"", 21 | "publish:next": "npm run prerelease && npm run build:prod && npx lerna publish --dist-tag=next --no-push --no-git-tag-version -m \"chore(@donews): publish %s release\"", 22 | "publish:rc": " npm run prerelease && npm run build:prod && npx lerna publish --dist-tag=rc -m \"chore(@donews): publish %s release\"", 23 | "publish:test": "npm run prerelease && npm run build:prod && npx lerna publish --dist-tag=test --no-push --no-git-tag-version --force-publish -m \"chore(@donews): publish %s release\"" 24 | }, 25 | "lint-staged": { 26 | "packages/**/*.{ts,json}": [ 27 | "prettier --ignore-path ./.prettierignore --write" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "19.8.1", 32 | "@commitlint/config-angular": "19.8.1", 33 | "@donews/eslint-config-donews": "1.0.3", 34 | "@eslint/js": "^9.21.0", 35 | "@types/gulp": "4.0.17", 36 | "@types/mocha": "10.0.10", 37 | "@types/node": "22.15.30", 38 | "@types/sinon": "17.0.4", 39 | "@typescript-eslint/eslint-plugin": "8.33.1", 40 | "@typescript-eslint/parser": "8.33.1", 41 | "cli-color": "2.0.4", 42 | "concurrently": "9.1.2", 43 | "delete-empty": "3.0.0", 44 | "eslint": "9.28.0", 45 | "eslint-config-prettier": "10.1.5", 46 | "eslint-plugin-import": "2.31.0", 47 | "eslint-plugin-prettier": "^5.2.3", 48 | "fancy-log": "2.0.0", 49 | "globals": "^16.0.0", 50 | "gulp": "5.0.1", 51 | "gulp-clean": "0.4.0", 52 | "gulp-sourcemaps": "3.0.0", 53 | "gulp-typescript": "5.0.1", 54 | "husky": "9.1.7", 55 | "lerna": "8.2.2", 56 | "lint-staged": "16.1.0", 57 | "mocha": "11.5.0", 58 | "nx": "^21.0.0", 59 | "nyc": "17.1.0", 60 | "prettier": "3.5.3", 61 | "reflect-metadata": "0.2.2", 62 | "sinon": "20.0.0", 63 | "ts-node": "10.9.2", 64 | "typescript": "5.8.3", 65 | "typescript-eslint": "^8.26.0" 66 | }, 67 | "packageManager": "pnpm@10.5.2", 68 | "nyc": { 69 | "include": [ 70 | "packages/**/*.ts" 71 | ], 72 | "exclude": [ 73 | "**/*.js", 74 | "**/*.d.ts", 75 | "**/*.spec.ts" 76 | ], 77 | "extension": [ 78 | ".ts" 79 | ], 80 | "require": [ 81 | "ts-node/register" 82 | ], 83 | "reporter": [ 84 | "text-summary", 85 | "html" 86 | ], 87 | "sourceMap": true, 88 | "instrument": true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/nestjs-config/README.md: -------------------------------------------------------------------------------- 1 | # nest config module 2 | 3 | 参照官方 @nestjs/config 4 | 5 | ## 描述 6 | 7 | 基于[js-yaml](https://github.com/nodeca/js-yaml)的[Nest](https://github.com/nestjs/nest)配置 module。 8 | 9 | ## 区别 10 | 11 | 1. 删除 dotenv 支持,转向 js-yaml 支持 12 | 2. 官方结构过于复杂 13 | 3. load 功能关闭,此项只能加载配置,直接配置文件就行,场景不明 14 | 15 | 待支持: 16 | 17 | 1. load 增加加载类的可能性,用于扩充 configService 18 | 2. configService 在读取时可能发生错误,因为 key 不存在,发生错误 19 | 20 | ## Install 21 | 22 | ```bash 23 | $ npm i @donews/nestjs-config 24 | ``` 25 | 26 | ## Quick Start 27 | 28 | 假定一个目录结构,其中包含`${rootPath}/config` 29 | 30 | ``` 31 | /root 32 | /config 33 | development.yaml 34 | /src 35 | app.module.ts 36 | ``` 37 | 38 | ### 1. 模块导入 39 | 40 | ```typescript 41 | import { Module } from '@nestjs/common'; 42 | import { ConfigModule } from '@donews/nestjs-config'; 43 | import { join } from 'path'; 44 | 45 | @Module({ 46 | import: [ 47 | ConfigModule.forRoot({ 48 | envFilePath: join(process.cwd(), './config/development.yaml'), 49 | }), 50 | ], 51 | }) 52 | export class AppModule {} 53 | ``` 54 | 55 | ### 2. ConfigModuleOptions 对应的值 56 | 57 | ```typescript 58 | export interface ConfigModuleOptions { 59 | // 是否将模块全局化 60 | isGlobal?: boolean; 61 | 62 | // 是否忽略环境变量配置文件 63 | ignoreEnvFile?: boolean; 64 | 65 | // 是否忽略检测 命令行输入的参数 检测 66 | ignoreCheckEnvVars?: boolean; 67 | 68 | // 配置文件地址,支持数组 69 | envFilePath?: string | string[]; 70 | 71 | // 编码格式,暂时无效 72 | encoding?: string; 73 | 74 | // 校验参数 joi包 75 | validationSchema?: 'Joi.AnySchema'; 76 | 77 | // joi 校验参数 78 | validationOptions?: 'ValidationOptions'; 79 | } 80 | ``` 81 | 82 | ### 3. 使用 83 | 84 | ```typescript 85 | import { ConfigService } from '@donews/nestjs-config'; 86 | 87 | class TestService { 88 | constructor(private readonly configService: ConfigService) {} 89 | 90 | say() { 91 | this.configService.get('PORT'); 92 | } 93 | } 94 | ``` 95 | 96 | ## License 97 | 98 | Nest is [MIT licensed](LICENSE). 99 | -------------------------------------------------------------------------------- /packages/nestjs-config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2021/4/11 3 | */ 4 | export * from './lib'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/config.constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Injection tokens 3 | */ 4 | export const CONFIGURATION_TOKEN = 'CONFIGURATION_TOKEN'; // 内部仓库读取方式 5 | 6 | export const CONFIGURATION_LOADER = 'CONFIGURATION_LOADER'; 7 | export const VALIDATED_ENV_LOADER = 'VALIDATED_ENV_LOADER'; 8 | 9 | export const PARTIAL_CONFIGURATION_KEY = 'PARTIAL_CONFIGURATION_KEY'; 10 | export const PARTIAL_CONFIGURATION_PROPNAME = 'KEY'; 11 | export const VALIDATED_ENV_PROPNAME = '_PROCESS_ENV_VALIDATED'; 12 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/config.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/25 3 | */ 4 | import * as fs from 'fs'; 5 | 6 | import { DynamicModule, FactoryProvider, Module } from '@nestjs/common'; 7 | import { ValidationOptions } from 'joi'; 8 | import { load } from 'js-yaml'; 9 | 10 | import { 11 | CONFIGURATION_TOKEN, 12 | VALIDATED_ENV_LOADER, 13 | VALIDATED_ENV_PROPNAME, 14 | } from './config.constants'; 15 | import { ConfigService } from './config.service'; 16 | import { ConfigModuleOptions } from './interfaces'; 17 | 18 | @Module({ 19 | providers: [ 20 | { 21 | provide: CONFIGURATION_TOKEN, 22 | useFactory: (): any => ({}), 23 | }, 24 | ConfigService, 25 | ], 26 | exports: [ConfigService], 27 | }) 28 | export class ConfigModule { 29 | static forRoot(options: ConfigModuleOptions = {}): DynamicModule { 30 | const { ignoreEnvFile, ignoreCheckEnvVars, validationSchema } = options; 31 | 32 | let validatedEnvConfig: Record | undefined = {}; 33 | 34 | // 是否 忽略 配置文档 35 | if (!ignoreEnvFile) { 36 | validatedEnvConfig = this.loadConfigFile(options); 37 | } 38 | 39 | // 是否 忽略 环境变量 40 | if (!ignoreCheckEnvVars) { 41 | validatedEnvConfig = Object.assign({}, validatedEnvConfig, process.env); 42 | } 43 | 44 | // 是否 是用 validate 检测 45 | if (validationSchema) { 46 | const validationOptions = this.getSchemaValidationOptions(options); 47 | const { error } = validationSchema.validate( 48 | validatedEnvConfig, 49 | validationOptions, 50 | ); 51 | 52 | if (error) { 53 | throw new Error(`Config validation error: ${error.message}`); 54 | } 55 | } 56 | 57 | const providers: FactoryProvider[] = []; 58 | // 将值 赋值进 内部缓存 59 | if (validatedEnvConfig) { 60 | const validatedEnvConfigLoader = { 61 | provide: VALIDATED_ENV_LOADER, 62 | useFactory: (innerCache: Record) => { 63 | innerCache[VALIDATED_ENV_PROPNAME] = validatedEnvConfig; 64 | if (validatedEnvConfig !== undefined) { 65 | for (const key of Object.keys(validatedEnvConfig)) { 66 | innerCache[key] = validatedEnvConfig[key]; 67 | } 68 | } 69 | }, 70 | inject: [CONFIGURATION_TOKEN], 71 | }; 72 | providers.push(validatedEnvConfigLoader); 73 | } 74 | 75 | return { 76 | module: ConfigModule, 77 | global: options.isGlobal, 78 | providers: providers, 79 | exports: [ConfigService, ...providers], 80 | }; 81 | } 82 | 83 | private static loadConfigFile( 84 | options: ConfigModuleOptions, 85 | ): Record { 86 | const envFilePaths: string[] = Array.isArray(options.envFilePath) 87 | ? options.envFilePath 88 | : [options.envFilePath ?? process.cwd() + '/config/default.yaml']; 89 | 90 | let config: ReturnType = {}; 91 | for (const envFilePath of envFilePaths) { 92 | if (fs.existsSync(envFilePath)) { 93 | config = Object.assign( 94 | load(fs.readFileSync(envFilePath, { encoding: 'utf8' })), 95 | config, 96 | ); 97 | } 98 | } 99 | return config; 100 | } 101 | 102 | private static getSchemaValidationOptions( 103 | options: ConfigModuleOptions, 104 | ): ValidationOptions { 105 | if (options.validationOptions) { 106 | if (typeof options.validationOptions?.allowUnknown === 'undefined') { 107 | options.validationOptions.allowUnknown = true; 108 | } 109 | return options.validationOptions || {}; 110 | } 111 | return { 112 | abortEarly: false, 113 | allowUnknown: true, 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/config.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/26 3 | */ 4 | import { Inject, Injectable } from '@nestjs/common'; 5 | import { get } from 'lodash'; 6 | 7 | import { CONFIGURATION_TOKEN } from './config.constants'; 8 | 9 | function isUndefined(value: any): boolean { 10 | return typeof value === 'undefined'; 11 | } 12 | 13 | @Injectable() 14 | export class ConfigService { 15 | constructor( 16 | @Inject(CONFIGURATION_TOKEN) 17 | private readonly internalConfig: Record = {}, 18 | ) {} 19 | 20 | /** 21 | * Get a configuration value (either custom configuration or process environment variable) 22 | * It returns a default value if the key does not exist. 23 | * @param propertyPath 24 | */ 25 | get(propertyPath: string): T | undefined; 26 | /** 27 | * Get a configuration value (either custom configuration or process environment variable) 28 | * It returns a default value if the key does not exist. 29 | * @param propertyPath 30 | * @param defaultValue 31 | */ 32 | get(propertyPath: string, defaultValue: T): T; 33 | /** 34 | * Get a configuration value (either custom configuration or process environment variable) 35 | * It returns a default value if the key does not exist. 36 | * @param propertyPath 37 | * @param defaultValue 38 | */ 39 | get(propertyPath: string, defaultValue?: T): T | undefined { 40 | const processValue = process.env?.[propertyPath]; 41 | // 这里其实应该去yargs取,暂时懒得改(这里有个限制,process.env只能传输字符串) 42 | if (!isUndefined(processValue)) { 43 | return processValue as unknown as T; 44 | } 45 | const internalValue = get(this.internalConfig, propertyPath); 46 | return isUndefined(internalValue) ? defaultValue : internalValue; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/30 3 | */ 4 | export * from './config.module'; 5 | export * from './config.service'; 6 | export * from './interfaces'; 7 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/interfaces/config-module-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/25 3 | */ 4 | import { AnySchema, ValidationOptions } from 'joi'; 5 | 6 | export interface ConfigModuleOptions { 7 | /** 8 | * If "true", registers `ConfigModule` as a global module. 9 | * See: https://docs.nestjs.com/modules#global-modules 10 | */ 11 | isGlobal?: boolean; 12 | 13 | /** 14 | * If "true", environment files (`.env`) will be ignored. 15 | */ 16 | ignoreEnvFile?: boolean; 17 | 18 | /** 19 | * If "true", predefined environment variables will not be validated. 20 | */ 21 | ignoreCheckEnvVars?: boolean; 22 | 23 | /** 24 | * Path to the environment file(s) to be loaded. 25 | */ 26 | envFilePath?: string | string[]; 27 | 28 | /** 29 | * Environment file encoding. 30 | */ 31 | encoding?: string; 32 | 33 | /** 34 | * Environment variables validation schema (Joi). 35 | */ 36 | validationSchema?: AnySchema; 37 | 38 | /** 39 | * Schema validation options. 40 | * See: https://hapi.dev/family/joi/?v=16.1.8#anyvalidatevalue-options 41 | */ 42 | validationOptions?: ValidationOptions; 43 | } 44 | -------------------------------------------------------------------------------- /packages/nestjs-config/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config-module-options.interface'; 2 | -------------------------------------------------------------------------------- /packages/nestjs-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@donews/nestjs-config", 3 | "version": "0.11.1", 4 | "description": "nestjs config module", 5 | "author": "toonewLi ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": {}, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/DoNewsCode/nestjs.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/DoNewsCode/nestjs/issues" 18 | }, 19 | "dependencies": { 20 | "joi": "17.13.3", 21 | "js-yaml": "4.1.0", 22 | "lodash": "4.17.21" 23 | }, 24 | "devDependencies": { 25 | "@nestjs/common": "11.1.3", 26 | "@nestjs/core": "11.1.3", 27 | "@nestjs/platform-express": "11.1.3", 28 | "@nestjs/testing": "11.1.3", 29 | "@types/js-yaml": "4.0.9", 30 | "@types/lodash": "4.17.17" 31 | }, 32 | "peerDependencies": { 33 | "@nestjs/common": "^11.0.0", 34 | "@nestjs/core": "^11.0.0" 35 | }, 36 | "gitHead": "4e0a8578a08c92cf2229122d58940c204034ba7b" 37 | } 38 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/e2e/load-env-file.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/26 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { INestApplication } from '@nestjs/common'; 7 | import { Test } from '@nestjs/testing'; 8 | 9 | import { AppModule } from '../src/app.module'; 10 | 11 | describe('envFilePath test', () => { 12 | let app: INestApplication; 13 | 14 | beforeEach(async () => { 15 | const module = await Test.createTestingModule({ 16 | imports: [AppModule.withEnvFile()], 17 | }).compile(); 18 | 19 | app = module.createNestApplication(); 20 | await app.init(); 21 | }); 22 | 23 | it(`should return env file variable`, () => { 24 | const configService = app 25 | .get(AppModule) 26 | .getConfigService(); 27 | assert.strictEqual(configService.get('PORT'), 4000); 28 | }); 29 | 30 | afterEach(async () => { 31 | await app.close(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/e2e/load-env-mutiple-file.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/26 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { INestApplication } from '@nestjs/common'; 7 | import { Test } from '@nestjs/testing'; 8 | 9 | import { AppModule } from '../src/app.module'; 10 | 11 | describe('envFilePath test', () => { 12 | let app: INestApplication; 13 | 14 | beforeEach(async () => { 15 | const module = await Test.createTestingModule({ 16 | imports: [AppModule.withMultipleEnvFile()], 17 | }).compile(); 18 | 19 | app = module.createNestApplication(); 20 | await app.init(); 21 | }); 22 | 23 | it(`should return env file variable`, () => { 24 | const configService = app 25 | .get(AppModule) 26 | .getConfigService(); 27 | assert.strictEqual(configService.get('PORT'), 4000); 28 | assert.strictEqual(configService.get('TIMEOUT'), 5000); 29 | assert.strictEqual(configService.get('OBJ1.filed1'), 2); 30 | 31 | assert.strictEqual(configService.get('DEV2_PORT'), 6000); 32 | assert.strictEqual(configService.get('DEV2_TIMEOUT'), 7000); 33 | }); 34 | 35 | afterEach(async () => { 36 | await app.close(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/e2e/process-env.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test } from '@nestjs/testing'; 5 | 6 | import { AppModule } from '../src/app.module'; 7 | 8 | /** 9 | * Created by Rain on 2021/4/9 10 | */ 11 | describe('env', () => { 12 | let app: INestApplication; 13 | 14 | beforeEach(async () => { 15 | process.env.PORT = '5000'; 16 | const module = await Test.createTestingModule({ 17 | imports: [AppModule.withEnvFile()], 18 | }).compile(); 19 | 20 | app = module.createNestApplication(); 21 | await app.init(); 22 | }); 23 | 24 | it(`should return env variable`, () => { 25 | const configService = app 26 | .get(AppModule) 27 | .getConfigService(); 28 | assert.strictEqual(configService.get('PORT'), '5000'); 29 | }); 30 | 31 | afterEach(async () => { 32 | delete process.env.PORT; 33 | await app.close(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/e2e/valid-schema.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/27 3 | */ 4 | import * as assert from 'assert'; 5 | import { join } from 'path'; 6 | 7 | import { INestApplication } from '@nestjs/common'; 8 | import { Test } from '@nestjs/testing'; 9 | 10 | import { ConfigService } from '../../lib'; 11 | import { AppModule } from '../src/app.module'; 12 | 13 | describe('envFilePath test', () => { 14 | let app: INestApplication; 15 | 16 | it(`should validate loaded env variables`, async () => { 17 | try { 18 | const module = await Test.createTestingModule({ 19 | imports: [AppModule.withSchemaValidation()], 20 | }).compile(); 21 | 22 | app = module.createNestApplication(); 23 | await app.init(); 24 | } catch (err) { 25 | assert.strictEqual( 26 | err.message, 27 | 'Config validation error: "PORT" is required. "DATABASE_NAME" is required', 28 | ); 29 | } 30 | }); 31 | 32 | it(`should parse loaded env variables`, async () => { 33 | const module = await Test.createTestingModule({ 34 | imports: [ 35 | AppModule.withSchemaValidation(join(__dirname, 'validation.yaml')), 36 | ], 37 | }).compile(); 38 | 39 | app = module.createNestApplication(); 40 | await app.init(); 41 | 42 | const configService = app.get(ConfigService); 43 | assert.strictEqual(configService.get('PORT'), 4000); 44 | assert.strictEqual(configService.get('DATABASE_NAME'), 'test'); 45 | assert.strictEqual(configService.get('FLAG_BOOLEAN'), true); 46 | 47 | assert.deepStrictEqual( 48 | configService.get<{ host: string; port: number }[]>('REDIS_CLUSTER_NODE'), 49 | [ 50 | { host: '127.0.0.1', port: 7000 }, 51 | { host: '127.0.0.1', port: 7001 }, 52 | ], 53 | ); 54 | 55 | assert.deepStrictEqual( 56 | configService.get<{ name: string; age: number }>('PERSON'), 57 | { name: 'toonewLi', age: 1000 }, 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/e2e/validation.yaml: -------------------------------------------------------------------------------- 1 | PORT: 4000 2 | DATABASE_NAME: test 3 | 4 | REDIS_CLUSTER_NODE: 5 | - port: 7000 6 | host: 127.0.0.1 7 | - port: 7001 8 | host: 127.0.0.1 9 | 10 | PERSON: 11 | name: toonewLi 12 | age: 1000 13 | 14 | FLAG_BOOLEAN: true 15 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/src/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/26 3 | */ 4 | import { join } from 'path'; 5 | 6 | import { DynamicModule, Module } from '@nestjs/common'; 7 | import * as Joi from 'joi'; 8 | 9 | import { ConfigModule, ConfigService } from '../../lib'; 10 | 11 | @Module({}) 12 | export class AppModule { 13 | constructor(private readonly configService: ConfigService) {} 14 | 15 | static withEnvFile(): DynamicModule { 16 | return { 17 | module: AppModule, 18 | imports: [ 19 | ConfigModule.forRoot({ 20 | isGlobal: true, 21 | envFilePath: join(__dirname, './config/development.yaml'), 22 | }), 23 | ], 24 | }; 25 | } 26 | 27 | static withMultipleEnvFile(): DynamicModule { 28 | return { 29 | module: AppModule, 30 | imports: [ 31 | ConfigModule.forRoot({ 32 | isGlobal: true, 33 | envFilePath: [ 34 | join(__dirname, './config/development.yaml'), 35 | join(__dirname, './config/development2.yaml'), 36 | ], 37 | }), 38 | ], 39 | }; 40 | } 41 | 42 | static withSchemaValidation(envFilePath?: string): DynamicModule { 43 | return { 44 | module: AppModule, 45 | imports: [ 46 | ConfigModule.forRoot({ 47 | envFilePath, 48 | validationSchema: Joi.object({ 49 | PORT: Joi.number().required(), 50 | DATABASE_NAME: Joi.string().required(), 51 | }), 52 | }), 53 | ], 54 | }; 55 | } 56 | 57 | getConfigService(): ConfigService { 58 | return this.configService; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/src/config/development-valid.yaml: -------------------------------------------------------------------------------- 1 | PORT: 4000 2 | DATABASE_NAME: test 3 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/src/config/development.yaml: -------------------------------------------------------------------------------- 1 | PORT: 4000 2 | TIMEOUT: 5000 3 | 4 | OBJ1: 5 | filed1: 2 6 | -------------------------------------------------------------------------------- /packages/nestjs-config/tests/src/config/development2.yaml: -------------------------------------------------------------------------------- 1 | DEV2_PORT: 6000 2 | DEV2_TIMEOUT: 7000 3 | -------------------------------------------------------------------------------- /packages/nestjs-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-logger/README.md: -------------------------------------------------------------------------------- 1 | # nest logger module 2 | 3 | ## 描述 4 | 5 | 基于 自我实现伪 json 输出 和 nest`内置logger`的[Nest](https://github.com/nestjs/nest)日志 module。 6 | 7 | 1. json (伪) 8 | 2. plain 格式输出基于 nest 内置 logger 9 | 10 | ## Install 11 | 12 | ```shell script 13 | $ npm i @donews/nestjs-logger 14 | ``` 15 | 16 | ## Quick Start 17 | 18 | 假定一个目录结构 19 | 20 | ``` 21 | /root 22 | /src 23 | app.module.ts 24 | ``` 25 | 26 | ### 1. 模块导入 27 | 28 | ```typescript 29 | import { LOGGER_TYPE, LoggerModule } from '@donews/nestjs-logger'; 30 | import { Module } from '@nestjs/common'; 31 | 32 | @Module({ 33 | imports: [ 34 | LoggerModule.forRoot({ 35 | loggerType: LOGGER_TYPE.JSON_MODEL, 36 | loggerLevel: 'debug', 37 | context: 'text', 38 | }), 39 | ], 40 | }) 41 | export class AppModule {} 42 | ``` 43 | 44 | ### 2. 增加 asynchronous provider 45 | 46 | ```typescript 47 | LoggerModule.forRootAsync({ 48 | imports: [ConfigModule], 49 | useFactory: (configService: ConfigService) => ({ 50 | loggerType: configService.get('loggerType'), 51 | loggerLevel: 'debug', 52 | context: 'text', 53 | }), 54 | inject: [ConfigService], 55 | }); 56 | ``` 57 | 58 | ### 3. 使用 59 | 60 | ```typescript 61 | import { LoggerInterface, LOGGER } from '@donews/nestjs-logger'; 62 | import { Inject, Injectable } from '@nestjs/common'; 63 | 64 | @Injectable() 65 | class TestService { 66 | constructor( 67 | @Inject(LOGGER) 68 | private readonly loggerService: LoggerInterface, 69 | ) {} 70 | 71 | say() { 72 | this.loggerService.info('message', 'context'); 73 | this.loggerService.error('error', 'trace', 'context'); 74 | } 75 | } 76 | ``` 77 | 78 | ### 4. 日志级别顺序 79 | 80 | | 级别 | 级别数 | 81 | | ------- | ------ | 82 | | error | 0 | 83 | | warn | 1 | 84 | | log | 2 | 85 | | info | 2 | 86 | | debug | 3 | 87 | | verbose | 4 | 88 | 89 | 级别从高到低 90 | 91 | ## License 92 | 93 | Nest is [MIT licensed](LICENSE). 94 | -------------------------------------------------------------------------------- /packages/nestjs-logger/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2021/4/11 3 | */ 4 | export * from './lib'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/constant.ts: -------------------------------------------------------------------------------- 1 | export const LOGGER = Symbol.for('logger'); 2 | export const LOGGER_MODULE_OPTIONS = Symbol.for('LOGGER_MODULE_OPTIONS'); 3 | 4 | export enum LOGGER_TYPE { 5 | JSON_MODEL = 'json', 6 | PLAIN_MODEL = 'plain', 7 | } 8 | 9 | export type LoggerLevel = 10 | | 'error' 11 | | 'warn' 12 | | 'log' 13 | | 'info' 14 | | 'verbose' 15 | | 'debug'; 16 | 17 | export const DonewsLoggerLevels: { [key in LoggerLevel]: number } = { 18 | error: 0, 19 | warn: 1, 20 | log: 2, 21 | info: 2, 22 | debug: 3, 23 | verbose: 4, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/30 3 | */ 4 | 5 | export * from './services/json-logger.service'; 6 | export * from './services/plain-logger.service'; 7 | 8 | export * from './interfaces'; 9 | export * from './constant'; 10 | export * from './logger.module'; 11 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/3/30 3 | */ 4 | export * from './logger-interface'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/interfaces/logger-interface.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService, Type } from '@nestjs/common'; 2 | import { ModuleMetadata } from '@nestjs/common/interfaces'; 3 | 4 | import { LOGGER_TYPE, LoggerLevel } from '../constant'; 5 | 6 | export type LogInfoType = string | Record; 7 | 8 | export interface LoggerInterface extends LoggerService { 9 | info(message: LogInfoType, context?: string): void; 10 | error(message: LogInfoType, trace?: string, context?: string): void; 11 | warn(message: LogInfoType, context?: string): any; 12 | debug?(message: LogInfoType, context?: string): any; 13 | verbose?(message: LogInfoType, context?: string): any; 14 | setLogLevel(logLevel: LoggerLevel): void; 15 | setLogContextRegex(contextList: string | RegExp | (string | RegExp)[]): void; 16 | } 17 | 18 | export interface LoggerOptions { 19 | context?: string; 20 | loggerType?: LOGGER_TYPE; 21 | loggerLevel?: LoggerLevel; 22 | loggerContextList?: string | RegExp | (string | RegExp)[]; 23 | } 24 | 25 | export interface LoggerOptionsFactory { 26 | createLoggerOptions(): Promise | LoggerOptions; 27 | } 28 | 29 | export interface LoggerModuleAsyncOptions 30 | extends Pick { 31 | name?: string; 32 | useExisting?: Type; 33 | useClass?: Type; 34 | useFactory?: (...args: any[]) => Promise | LoggerOptions; 35 | inject?: any[]; 36 | } 37 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | FactoryProvider, 4 | Global, 5 | Module, 6 | Provider, 7 | Type, 8 | ValueProvider, 9 | } from '@nestjs/common'; 10 | 11 | import { LOGGER, LOGGER_MODULE_OPTIONS, LOGGER_TYPE } from './constant'; 12 | import { 13 | LoggerModuleAsyncOptions, 14 | LoggerOptions, 15 | LoggerOptionsFactory, 16 | } from './interfaces'; 17 | import { JsonLoggerService, PlainLoggerService } from './services'; 18 | import { CommonUtil } from './util/common.util'; 19 | 20 | @Global() 21 | @Module({}) 22 | export class LoggerModule { 23 | static forRoot(options: LoggerOptions): DynamicModule { 24 | const providers: Provider[] = []; 25 | 26 | const loggerOptionsProvider: ValueProvider = { 27 | provide: LOGGER_MODULE_OPTIONS, 28 | useValue: options, 29 | }; 30 | providers.push(loggerOptionsProvider); 31 | 32 | const LoggerServiceProvider: FactoryProvider = { 33 | provide: LOGGER, 34 | useFactory: (_options: LoggerOptions) => { 35 | CommonUtil.checkLogType(_options.loggerType); 36 | 37 | let LoggerService: any; 38 | if (_options.loggerType === LOGGER_TYPE.JSON_MODEL) { 39 | LoggerService = JsonLoggerService; 40 | } 41 | if (_options.loggerType === LOGGER_TYPE.PLAIN_MODEL) { 42 | LoggerService = PlainLoggerService; 43 | } 44 | const logInstance = new LoggerService(_options); 45 | logInstance.setLogLevel(_options.loggerLevel); 46 | return logInstance; 47 | }, 48 | inject: [LOGGER_MODULE_OPTIONS], 49 | }; 50 | providers.push(LoggerServiceProvider); 51 | 52 | return { 53 | module: LoggerModule, 54 | providers: providers, 55 | exports: providers, 56 | }; 57 | } 58 | 59 | static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule { 60 | const providers: Provider[] = []; 61 | 62 | const LoggerServiceProvider: FactoryProvider = { 63 | provide: LOGGER, 64 | useFactory: (_options: LoggerOptions) => { 65 | CommonUtil.checkLogType(_options.loggerType); 66 | 67 | let LoggerService: any; 68 | if (_options.loggerType === LOGGER_TYPE.JSON_MODEL) { 69 | LoggerService = JsonLoggerService; 70 | } 71 | if (_options.loggerType === LOGGER_TYPE.PLAIN_MODEL) { 72 | LoggerService = PlainLoggerService; 73 | } 74 | const logInstance = new LoggerService(_options); 75 | logInstance.setLogLevel(_options.loggerLevel); 76 | return logInstance; 77 | }, 78 | inject: [LOGGER_MODULE_OPTIONS], 79 | }; 80 | providers.push(LoggerServiceProvider); 81 | 82 | providers.push(...this.createAsyncProviders(options)); 83 | return { 84 | module: LoggerModule, 85 | imports: options.imports, 86 | providers: providers, 87 | exports: providers, 88 | }; 89 | } 90 | 91 | private static createAsyncProviders( 92 | options: LoggerModuleAsyncOptions, 93 | ): Provider[] { 94 | if (options.useExisting || options.useFactory) { 95 | return [this.createAsyncOptionsProvider(options)]; 96 | } 97 | const useClass = options.useClass as Type; 98 | return [ 99 | this.createAsyncOptionsProvider(options), 100 | { 101 | provide: useClass, 102 | useClass, 103 | }, 104 | ]; 105 | } 106 | 107 | private static createAsyncOptionsProvider( 108 | options: LoggerModuleAsyncOptions, 109 | ): Provider { 110 | if (options.useFactory) { 111 | return { 112 | provide: LOGGER_MODULE_OPTIONS, 113 | useFactory: options.useFactory, 114 | inject: options.inject || [], 115 | }; 116 | } 117 | 118 | const inject = [ 119 | (options.useClass || options.useExisting) as Type, 120 | ]; 121 | return { 122 | provide: LOGGER_MODULE_OPTIONS, 123 | useFactory: async ( 124 | optionsFactory: LoggerOptionsFactory, 125 | ): Promise => await optionsFactory.createLoggerOptions(), 126 | inject, 127 | }; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './json-logger.service'; 2 | export * from './plain-logger.service'; 3 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/services/json-logger.service.ts: -------------------------------------------------------------------------------- 1 | import * as util from 'util'; 2 | 3 | import { DonewsLoggerLevels, LoggerLevel } from '../constant'; 4 | import { LoggerInterface, LoggerOptions, LogInfoType } from '../interfaces'; 5 | import { CommonUtil } from '../util/common.util'; 6 | 7 | export class JsonLoggerService implements LoggerInterface { 8 | private loggerLevel: LoggerLevel; 9 | private loggerContextRegexList: RegExp[] = []; 10 | 11 | private readonly context: string; 12 | 13 | constructor(options: LoggerOptions) { 14 | this.context = options.context || 'UserScope'; 15 | this.setLogLevel(options.loggerLevel); 16 | this.setLogContextRegex(options.loggerContextList); 17 | } 18 | 19 | private static isJson(str: string): boolean { 20 | if (typeof str !== 'string') { 21 | return false; 22 | } 23 | return /^{|(^\[.*]$)/gi.test(str); 24 | } 25 | 26 | /** 27 | * 根据 log level 和 context 判断是否需要输出日志 28 | * @param {LoggerLevel}level 29 | * @param {string}context 30 | */ 31 | isPrint(level: LoggerLevel, context?: string): boolean { 32 | if (CommonUtil.checkContextByRegex(this.loggerContextRegexList, context)) { 33 | return true; 34 | } 35 | return DonewsLoggerLevels[this.loggerLevel] >= DonewsLoggerLevels[level]; 36 | } 37 | 38 | private static prepare(value: LogInfoType): string { 39 | let message = ''; 40 | if (typeof value === 'object') { 41 | try { 42 | message = JSON.stringify(value); 43 | } catch (err) { 44 | message = util.inspect(value); 45 | } 46 | } 47 | if (typeof value === 'string') { 48 | message = value; 49 | } 50 | 51 | // 防止message字段答应出json结构,导致es解析失败 52 | return JsonLoggerService.isJson(message) ? '\\' + message : message; 53 | } 54 | 55 | error(message: string, trace?: string, context?: string): void { 56 | if (!this.isPrint('error', context)) { 57 | return; 58 | } 59 | const formatted = JsonLoggerService.prepare(message); 60 | const formattedTrace = JsonLoggerService.prepare(trace); 61 | const formattedContext = JsonLoggerService.prepare(context); 62 | this.callFunction('error', formatted, { 63 | trace: formattedTrace, 64 | context: formattedContext || this.context, 65 | }); 66 | } 67 | 68 | warn(message: LogInfoType, context?: string): void { 69 | if (!this.isPrint('warn', context)) { 70 | return; 71 | } 72 | const formatted = JsonLoggerService.prepare(message); 73 | const formattedContext = JsonLoggerService.prepare(context); 74 | this.callFunction('warn', formatted, { 75 | context: formattedContext || this.context, 76 | }); 77 | } 78 | 79 | log(message: LogInfoType, context?: string): void { 80 | if (!this.isPrint('log', context)) { 81 | return; 82 | } 83 | const formatted = JsonLoggerService.prepare(message); 84 | const formattedContext = JsonLoggerService.prepare(context); 85 | this.callFunction('log', formatted, { 86 | context: formattedContext || this.context, 87 | }); 88 | } 89 | 90 | info(message: LogInfoType, context?: string): void { 91 | if (!this.isPrint('info', context)) { 92 | return; 93 | } 94 | const formatted = JsonLoggerService.prepare(message); 95 | const formattedContext = JsonLoggerService.prepare(context); 96 | this.callFunction('info', formatted, { 97 | context: formattedContext || this.context, 98 | }); 99 | } 100 | 101 | debug(message: LogInfoType, context?: string): void { 102 | if (!this.isPrint('debug', context)) { 103 | return; 104 | } 105 | const formatted = JsonLoggerService.prepare(message); 106 | const formattedContext = JsonLoggerService.prepare(context); 107 | this.callFunction('debug', formatted, { 108 | context: formattedContext || this.context, 109 | }); 110 | } 111 | 112 | verbose(message: LogInfoType, context?: string): void { 113 | if (!this.isPrint('verbose', context)) { 114 | return; 115 | } 116 | const formatted = JsonLoggerService.prepare(message); 117 | const formattedContext = JsonLoggerService.prepare(context); 118 | this.callFunction('verbose', formatted, { 119 | context: formattedContext || this.context, 120 | }); 121 | } 122 | 123 | callFunction(method: string, message: string, ...meta: any[]): void { 124 | const mObject = 125 | JSON.stringify( 126 | Object.assign( 127 | { timestamp: new Date().toISOString(), level: method, message }, 128 | ...meta, 129 | ), 130 | ) + '\n'; 131 | 132 | if (method === 'error') { 133 | process.stderr.write(mObject); 134 | } else { 135 | process.stdout.write(mObject); 136 | } 137 | } 138 | 139 | setLogLevel(logLevel: LoggerLevel): void { 140 | this.loggerLevel = CommonUtil.checkLogLevel(logLevel); 141 | } 142 | 143 | setLogContextRegex(contextList: string | RegExp | (string | RegExp)[]): void { 144 | this.loggerContextRegexList = 145 | CommonUtil.generateContextRegexList(contextList); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/services/plain-logger.service.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogger } from '@nestjs/common'; 2 | 3 | import { DonewsLoggerLevels, LoggerLevel } from '../constant'; 4 | import { LoggerInterface, LoggerOptions } from '../interfaces'; 5 | import { CommonUtil } from '../util/common.util'; 6 | 7 | export class PlainLoggerService 8 | extends ConsoleLogger 9 | implements LoggerInterface 10 | { 11 | private loggerLevel: LoggerLevel; 12 | private loggerContextRegexList: RegExp[] = []; 13 | 14 | constructor(loggerOptions: LoggerOptions) { 15 | super(loggerOptions.context || 'UserScope'); 16 | this.setLogLevel(loggerOptions.loggerLevel); 17 | this.setLogContextRegex(loggerOptions.loggerContextList); 18 | } 19 | 20 | isPrint(level: LoggerLevel, context?: string): boolean { 21 | if (CommonUtil.checkContextByRegex(this.loggerContextRegexList, context)) { 22 | return true; 23 | } 24 | return DonewsLoggerLevels[this.loggerLevel] >= DonewsLoggerLevels[level]; 25 | } 26 | 27 | error(message: string, trace: string = '', context: string = ''): void { 28 | if (!this.isPrint('error', context)) { 29 | return; 30 | } 31 | if (!context) { 32 | context = this.context; 33 | } 34 | super.error(message, trace, context); 35 | } 36 | 37 | warn(message: string, context?: string): void { 38 | if (!this.isPrint('warn', context)) { 39 | return; 40 | } 41 | if (!context) { 42 | context = this.context; 43 | } 44 | super.warn(message, context); 45 | } 46 | 47 | log(message: string, context?: string): void { 48 | if (!this.isPrint('log', context)) { 49 | return; 50 | } 51 | if (!context) { 52 | context = this.context; 53 | } 54 | super.log(message, context); 55 | } 56 | 57 | info(message: string, context?: string): void { 58 | if (!this.isPrint('info', context)) { 59 | return; 60 | } 61 | if (!context) { 62 | context = this.context; 63 | } 64 | super.log(message, context); 65 | } 66 | 67 | debug(message: string, context?: string): void { 68 | if (!this.isPrint('debug', context)) { 69 | return; 70 | } 71 | if (!context) { 72 | context = this.context; 73 | } 74 | super.debug(message, context); 75 | } 76 | 77 | verbose(message: string, context?: string): void { 78 | if (!this.isPrint('verbose', context)) { 79 | return; 80 | } 81 | if (!context) { 82 | context = this.context; 83 | } 84 | super.verbose(message, context); 85 | } 86 | 87 | setLogLevel(logLevel: LoggerLevel): void { 88 | this.loggerLevel = CommonUtil.checkLogLevel(logLevel); 89 | } 90 | 91 | setLogContextRegex(contextList: string | RegExp | (string | RegExp)[]): void { 92 | this.loggerContextRegexList = 93 | CommonUtil.generateContextRegexList(contextList); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/nestjs-logger/lib/util/common.util.ts: -------------------------------------------------------------------------------- 1 | import { DonewsLoggerLevels, LOGGER_TYPE, LoggerLevel } from '../constant'; 2 | 3 | /** 4 | * Created by Rain on 2020/6/16 5 | */ 6 | export class CommonUtil { 7 | static checkContextByRegex(regexList: RegExp[], context: string): boolean { 8 | if (!regexList || !Array.isArray(regexList)) { 9 | return false; 10 | } 11 | if (regexList.length >= 0) { 12 | for (const regex of regexList) { 13 | if (regex.test(context)) { 14 | return true; 15 | } 16 | } 17 | } 18 | return false; 19 | } 20 | 21 | static generateContextRegexList( 22 | contextList: string | RegExp | (string | RegExp)[], 23 | ): RegExp[] { 24 | let array = []; 25 | if (typeof contextList === 'undefined') { 26 | array = []; 27 | } else if (!Array.isArray(contextList)) { 28 | array.push(contextList); 29 | } else { 30 | array = array.concat(contextList); 31 | } 32 | 33 | return array.reduce((acc, context: string | RegExp) => { 34 | if (typeof context === 'string') { 35 | acc.push(new RegExp(context)); 36 | } else if (context instanceof RegExp) { 37 | acc.push(context); 38 | } else { 39 | throw new Error('please input string or regex'); 40 | } 41 | return acc; 42 | }, []); 43 | } 44 | 45 | static checkLogLevel(logLevel: LoggerLevel): LoggerLevel { 46 | if (typeof logLevel !== 'string') { 47 | throw new Error('Please logLevel is string'); 48 | } 49 | if (typeof DonewsLoggerLevels[logLevel] !== 'number') { 50 | throw new Error(`Please input correct logLevel,current:${logLevel}`); 51 | } 52 | return logLevel || 'debug'; 53 | } 54 | 55 | static checkLogType(logType: string): boolean { 56 | if ( 57 | LOGGER_TYPE.PLAIN_MODEL === logType || 58 | LOGGER_TYPE.JSON_MODEL === logType 59 | ) { 60 | return true; 61 | } else { 62 | throw new Error('Please input correct logger type[json,plain]!'); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/nestjs-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@donews/nestjs-logger", 3 | "version": "0.11.1", 4 | "description": "nestjs logger module", 5 | "author": "toonewLi ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": {}, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/DoNewsCode/nestjs.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/DoNewsCode/nestjs/issues" 18 | }, 19 | "devDependencies": { 20 | "@nestjs/common": "11.1.3", 21 | "@nestjs/core": "11.1.3", 22 | "@nestjs/platform-express": "11.1.3", 23 | "@nestjs/testing": "11.1.3" 24 | }, 25 | "peerDependencies": { 26 | "@nestjs/common": "^11.0.0", 27 | "@nestjs/core": "^11.0.0" 28 | }, 29 | "gitHead": "4e0a8578a08c92cf2229122d58940c204034ba7b" 30 | } 31 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/async-module-json-logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { INestApplication } from '@nestjs/common'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | 6 | import { LOGGER, LoggerInterface, LoggerModule, LoggerOptions } from '../lib'; 7 | 8 | import { ConfigModule } from './config/config.module'; 9 | import { ConfigService } from './config/config.service'; 10 | 11 | describe('json-logger.service.spec', () => { 12 | let app: INestApplication; 13 | 14 | beforeEach(async () => { 15 | const moduleFixture: TestingModule = await Test.createTestingModule({ 16 | imports: [ 17 | LoggerModule.forRootAsync({ 18 | name: 'test', 19 | useFactory(configService: ConfigService): LoggerOptions { 20 | return { 21 | loggerType: configService.getJson(), 22 | loggerLevel: 'debug', 23 | context: 'test', 24 | }; 25 | }, 26 | inject: [ConfigService], 27 | imports: [ConfigModule], 28 | }), 29 | ], 30 | }).compile(); 31 | 32 | app = moduleFixture.createNestApplication(); 33 | await app.init(); 34 | }); 35 | 36 | it('test', () => { 37 | const log = app.get<'string', LoggerInterface>(LOGGER); 38 | log.info(JSON.stringify({ a: 1 })); 39 | }); 40 | 41 | it('log type error', async () => { 42 | try { 43 | const moduleFixture: TestingModule = await Test.createTestingModule({ 44 | imports: [ 45 | LoggerModule.forRootAsync({ 46 | name: 'test', 47 | useFactory(configService: ConfigService): LoggerOptions { 48 | return { 49 | loggerType: '' as any, 50 | loggerLevel: 'debug', 51 | context: 'test', 52 | }; 53 | }, 54 | inject: [ConfigService], 55 | imports: [ConfigModule], 56 | }), 57 | ], 58 | }).compile(); 59 | 60 | app = moduleFixture.createNestApplication(); 61 | await app.init(); 62 | } catch (e) { 63 | assert.strictEqual( 64 | e.message, 65 | 'Please input correct logger type[json,plain]!', 66 | ); 67 | } 68 | }); 69 | 70 | it('log level error', async () => { 71 | try { 72 | const moduleFixture: TestingModule = await Test.createTestingModule({ 73 | imports: [ 74 | LoggerModule.forRootAsync({ 75 | name: 'test', 76 | useFactory(configService: ConfigService): LoggerOptions { 77 | return { 78 | loggerType: configService.getJson(), 79 | loggerLevel: 'x' as any, 80 | context: 'test', 81 | }; 82 | }, 83 | inject: [ConfigService], 84 | imports: [ConfigModule], 85 | }), 86 | ], 87 | }).compile(); 88 | 89 | app = moduleFixture.createNestApplication(); 90 | await app.init(); 91 | } catch (e) { 92 | assert.strictEqual(e.message, 'Please input correct logLevel,current:x'); 93 | } 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | @Module({ 6 | providers: [ConfigService], 7 | exports: [ConfigService], 8 | }) 9 | export class ConfigModule {} 10 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { LOGGER_TYPE } from '../../lib'; 4 | 5 | @Injectable() 6 | export class ConfigService { 7 | getJson(): LOGGER_TYPE { 8 | return LOGGER_TYPE.JSON_MODEL; 9 | } 10 | getPlain(): LOGGER_TYPE { 11 | return LOGGER_TYPE.PLAIN_MODEL; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/json-logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { LOGGER, LOGGER_TYPE, LoggerInterface, LoggerModule } from '../lib'; 5 | 6 | describe('json-logger.service.spec', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [ 12 | LoggerModule.forRoot({ 13 | loggerType: LOGGER_TYPE.JSON_MODEL, 14 | loggerLevel: 'verbose', 15 | context: 'test', 16 | }), 17 | ], 18 | }).compile(); 19 | 20 | app = moduleFixture.createNestApplication(); 21 | await app.init(); 22 | }); 23 | 24 | it('test', () => { 25 | const log = app.get<'string', LoggerInterface>(LOGGER); 26 | log.info(JSON.stringify({ a: 1 })); 27 | }); 28 | 29 | it('just print ', () => { 30 | const log = app.get<'string', LoggerInterface>(LOGGER); 31 | log.error('error', 'error.stack', 'error'); 32 | log.warn('warn', 'warn'); 33 | log.log('log', 'log'); 34 | log.info('info', 'info'); 35 | log.debug('debug', 'debug'); 36 | log.verbose('verbose', 'verbose'); 37 | 38 | log.verbose(JSON.stringify({ test: 1 }), JSON.stringify({ test: 1 })); 39 | log.verbose({ test: 1 }, JSON.stringify({ test: 1 })); 40 | log.verbose([{ test: 1 }], 'context'); 41 | }); 42 | 43 | it('setLogContextRegex test', () => { 44 | const log = app.get<'string', LoggerInterface>(LOGGER); 45 | log.setLogLevel('error'); 46 | 47 | // debug 默认不显示 48 | log.setLogContextRegex(['test1']); 49 | log.verbose(JSON.stringify({ test: '条件:[test1] 不出' }), ''); 50 | log.verbose(JSON.stringify({ test: '条件:[test1] 出' }), 'test1'); // 出 51 | 52 | log.setLogContextRegex('test2'); 53 | log.verbose(JSON.stringify({ test: '条件:test2 不出' }), 'test'); // 不出 54 | log.verbose(JSON.stringify({ test: '条件:test2 出' }), 'test2'); // 出 55 | 56 | log.setLogContextRegex(/test3/); 57 | log.verbose(JSON.stringify({ test: '条件:/test3/ 不出' }), 'test'); // 不出 58 | log.verbose(JSON.stringify({ test: '条件:/test3/ 出' }), 'test3'); // 出 59 | 60 | log.setLogContextRegex([/test4/]); 61 | log.verbose(JSON.stringify({ test: '条件:[test4] 不出' }), 'test'); // 不出 62 | log.verbose(JSON.stringify({ test: '条件:[test4] 出' }), 'test4'); // 出 63 | }); 64 | 65 | it('module init contextList test', async () => { 66 | const log = await generatorLog(['regex test']); 67 | log.debug(`条件:['regex test'] 不出`, 's'); 68 | log.debug(`条件:['regex test'] 出`, 'regex test'); 69 | 70 | const log2 = await generatorLog([/regex te.*/]); 71 | log2.debug(`条件:[/regex te.*/] 不出`, 's'); 72 | log2.debug(`条件:[/regex te.*/] 出`, 'regex test'); 73 | 74 | const log3 = await generatorLog('regex test'); 75 | log3.debug(`条件:regex test 不出`, 's'); 76 | log3.debug(`条件:regex test 出`, 'regex test'); 77 | 78 | const log4 = await generatorLog(/regex te.*/); 79 | log4.debug(`条件:regex test 不出`, 's'); 80 | log4.debug(`条件:regex test 出`, 'regex test'); 81 | 82 | async function generatorLog(contextList) { 83 | const moduleFixture: TestingModule = await Test.createTestingModule({ 84 | imports: [ 85 | LoggerModule.forRoot({ 86 | loggerType: LOGGER_TYPE.JSON_MODEL, 87 | loggerLevel: 'error', 88 | context: 'test', 89 | loggerContextList: contextList, 90 | }), 91 | ], 92 | }).compile(); 93 | 94 | app = moduleFixture.createNestApplication(); 95 | await app.init(); 96 | return app.get<'string', LoggerInterface>(LOGGER); 97 | } 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/plain-logger.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { LOGGER, LOGGER_TYPE, LoggerInterface, LoggerModule } from '../lib'; 5 | 6 | describe('plain-logger.service test', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [ 12 | LoggerModule.forRoot({ 13 | loggerType: LOGGER_TYPE.PLAIN_MODEL, 14 | loggerLevel: 'verbose', 15 | context: 'test', 16 | }), 17 | ], 18 | }).compile(); 19 | 20 | app = moduleFixture.createNestApplication(); 21 | await app.init(); 22 | }); 23 | 24 | it('test', () => { 25 | const log = app.get<'string', LoggerInterface>(LOGGER); 26 | // 此输出依赖官方logger简版试下,普通启动和test启动不一样 27 | // 因为官方的test 方式启动 logger 不完整,test中删除了info,warn的输出,仅仅保留了error 28 | // sourcePath: @nestjs/testing/services/testing-logger.service.js 29 | log.info(JSON.stringify({ a: 1 })); 30 | log.warn(JSON.stringify({ a: 1 })); 31 | log.error(JSON.stringify({ a: 1 })); 32 | }); 33 | 34 | it('just print ', () => { 35 | const log = app.get<'string', LoggerInterface>(LOGGER); 36 | log.error('error', 'error.stack', 'error'); 37 | log.warn('warn', 'warn'); 38 | log.log('log', 'log'); 39 | log.info('info', 'info'); 40 | log.debug('debug', 'debug'); 41 | log.verbose('verbose', 'verbose'); 42 | }); 43 | 44 | it('setLogContextRegex test', () => { 45 | const log = app.get<'string', LoggerInterface>(LOGGER); 46 | log.setLogLevel('error'); 47 | 48 | // debug 默认不显示 49 | log.setLogContextRegex(['test1']); 50 | log.verbose(JSON.stringify({ test: '条件:[test1] 不出' }), ''); 51 | log.verbose(JSON.stringify({ test: '条件:[test1] 出' }), 'test1'); // 出 52 | 53 | log.setLogContextRegex('test2'); 54 | log.verbose(JSON.stringify({ test: '条件:test2 不出' }), 'test'); // 不出 55 | log.verbose(JSON.stringify({ test: '条件:test2 出' }), 'test2'); // 出 56 | 57 | log.setLogContextRegex(/test3/); 58 | log.verbose(JSON.stringify({ test: '条件:/test3/ 不出' }), 'test'); // 不出 59 | log.verbose(JSON.stringify({ test: '条件:/test3/ 出' }), 'test3'); // 出 60 | 61 | log.setLogContextRegex([/test4/]); 62 | log.verbose(JSON.stringify({ test: '条件:[test4] 不出' }), 'test'); // 不出 63 | log.verbose(JSON.stringify({ test: '条件:[test4] 出' }), 'test4'); // 出 64 | }); 65 | 66 | it('module init contextList test', async () => { 67 | const log = await generatorLog(['regex test']); 68 | log.debug(`条件:['regex test'] 不出`, 's'); 69 | log.debug(`条件:['regex test'] 出`, 'regex test'); 70 | 71 | const log2 = await generatorLog([/regex te.*/]); 72 | log2.debug(`条件:[/regex te.*/] 不出`, 's'); 73 | log2.debug(`条件:[/regex te.*/] 出`, 'regex test'); 74 | 75 | const log3 = await generatorLog('regex test'); 76 | log3.debug(`条件:regex test 不出`, 's'); 77 | log3.debug(`条件:regex test 出`, 'regex test'); 78 | 79 | const log4 = await generatorLog(/regex te.*/); 80 | log4.debug(`条件:regex test 不出`, 's'); 81 | log4.debug(`条件:regex test 出`, 'regex test'); 82 | 83 | async function generatorLog(contextList) { 84 | const moduleFixture: TestingModule = await Test.createTestingModule({ 85 | imports: [ 86 | LoggerModule.forRoot({ 87 | loggerType: LOGGER_TYPE.PLAIN_MODEL, 88 | loggerLevel: 'error', 89 | context: 'test', 90 | loggerContextList: contextList, 91 | }), 92 | ], 93 | }).compile(); 94 | 95 | app = moduleFixture.createNestApplication(); 96 | await app.init(); 97 | return app.get<'string', LoggerInterface>(LOGGER); 98 | } 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/nestjs-logger/test/util/common.util.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/16 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { CommonUtil } from '../../lib/util/common.util'; 7 | 8 | describe('common util ', () => { 9 | describe('checkContextRegex', () => { 10 | it('null context', () => { 11 | const regex = null; 12 | assert.strictEqual( 13 | CommonUtil.checkContextByRegex(regex, 'context1'), 14 | false, 15 | ); 16 | assert.strictEqual( 17 | CommonUtil.checkContextByRegex(regex, 'context1'), 18 | false, 19 | ); 20 | }); 21 | it('empty array context', () => { 22 | const regex = []; 23 | assert.strictEqual( 24 | CommonUtil.checkContextByRegex(regex, 'context1'), 25 | false, 26 | ); 27 | assert.strictEqual( 28 | CommonUtil.checkContextByRegex(regex, 'context1'), 29 | false, 30 | ); 31 | }); 32 | 33 | it('regex', () => { 34 | const regex = [/context1.*ts/]; 35 | assert.strictEqual( 36 | CommonUtil.checkContextByRegex(regex, 'context1-xxx.ts'), 37 | true, 38 | ); 39 | assert.strictEqual( 40 | CommonUtil.checkContextByRegex(regex, 'context2-xx.ts'), 41 | false, 42 | ); 43 | }); 44 | }); 45 | 46 | describe('getContextRegexList', () => { 47 | it('字符串 数字', () => { 48 | const regexList: RegExp[] = CommonUtil.generateContextRegexList('0'); 49 | assert.strictEqual(regexList[0].test('0'), true); 50 | assert.strictEqual(regexList[0].test('1'), false); 51 | }); 52 | 53 | it('字符串 数字 数组', () => { 54 | const regexList: RegExp[] = CommonUtil.generateContextRegexList('0'); 55 | assert.strictEqual(regexList[0].test('0'), true); 56 | assert.strictEqual(regexList[0].test('1'), false); 57 | }); 58 | 59 | it('字符串 ', () => { 60 | const regexList: RegExp[] = CommonUtil.generateContextRegexList('test'); 61 | assert.strictEqual(regexList[0].test('test'), true); 62 | assert.strictEqual(regexList[0].test('tes2'), false); 63 | }); 64 | 65 | it('字符串 数组', () => { 66 | const regexList: RegExp[] = CommonUtil.generateContextRegexList(['test']); 67 | assert.strictEqual(regexList[0].test('test'), true); 68 | assert.strictEqual(regexList[0].test('tes2'), false); 69 | }); 70 | 71 | it('正则', () => { 72 | const regexList: RegExp[] = CommonUtil.generateContextRegexList(/te.*1/); 73 | assert.strictEqual(regexList[0].test('testx1'), true); 74 | assert.strictEqual(regexList[0].test('testx2'), false); 75 | }); 76 | 77 | it('正则数组', () => { 78 | const regexList: RegExp[] = CommonUtil.generateContextRegexList([ 79 | /te.*1/, 80 | ]); 81 | assert.strictEqual(regexList[0].test('testx1'), true); 82 | assert.strictEqual(regexList[0].test('testx2'), false); 83 | }); 84 | 85 | it('混合参数 数组', () => { 86 | const regexList: RegExp[] = CommonUtil.generateContextRegexList([ 87 | '0', 88 | 'test', 89 | /te.*1/, 90 | ]); 91 | 92 | assert.strictEqual(regexList[0].test('0'), true); 93 | assert.strictEqual(regexList[0].test('1'), false); 94 | 95 | assert.strictEqual(regexList[1].test('test'), true); 96 | assert.strictEqual(regexList[1].test('tes2'), false); 97 | 98 | assert.strictEqual(regexList[2].test('testx1'), true); 99 | assert.strictEqual(regexList[2].test('testx2'), false); 100 | }); 101 | }); 102 | 103 | describe('getLogLevel', () => { 104 | it('empty null', () => { 105 | try { 106 | const str = null as any; 107 | CommonUtil.checkLogLevel(str); 108 | } catch (err) { 109 | assert.strictEqual(err.message, 'Please logLevel is string'); 110 | } 111 | }); 112 | 113 | it('input number error', () => { 114 | try { 115 | const str = 0 as any; 116 | CommonUtil.checkLogLevel(str); 117 | } catch (err) { 118 | assert.strictEqual(err.message, 'Please logLevel is string'); 119 | } 120 | }); 121 | 122 | it('loglevel spell error', () => { 123 | const str = 'debug1' as any; 124 | try { 125 | CommonUtil.checkLogLevel(str); 126 | } catch (err) { 127 | assert.strictEqual( 128 | err.message, 129 | `Please input correct logLevel,current:${str}`, 130 | ); 131 | } 132 | }); 133 | 134 | it('all logLevel', () => { 135 | assert.strictEqual(CommonUtil.checkLogLevel('error'), 'error'); 136 | assert.strictEqual(CommonUtil.checkLogLevel('verbose'), 'verbose'); 137 | assert.strictEqual(CommonUtil.checkLogLevel('info'), 'info'); 138 | assert.strictEqual(CommonUtil.checkLogLevel('log'), 'log'); 139 | assert.strictEqual(CommonUtil.checkLogLevel('debug'), 'debug'); 140 | assert.strictEqual(CommonUtil.checkLogLevel('verbose'), 'verbose'); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /packages/nestjs-logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-prom/README.md: -------------------------------------------------------------------------------- 1 | # nest prometheus module 2 | 3 | 取消多 register 设计,没啥必要,感觉不到多个 register 的用处和意义, 4 | 并且不同 register 同 name 合并时,会直接 throw 没啥意义,以后由更加完善的需求再研究吧 5 | 6 | ## 描述 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm i @donews/nestjs-prom prom-client 12 | ``` 13 | 14 | ## Quick Start 15 | 16 | 假定一个目录结构 17 | 18 | ``` 19 | /root 20 | /src 21 | app.module.ts 22 | ``` 23 | 24 | ### 1. 模块导入 25 | 26 | ```typescript 27 | import { LoggerModule } from '@donews/nestjs-prom'; 28 | import { Module } from '@nestjs/common'; 29 | 30 | @Module({ 31 | imports: [ 32 | PromModule.forRoot( 33 | { 34 | withDefaultsMetrics: false, 35 | }, 36 | [ 37 | { 38 | type: MetricType.Counter, 39 | configuration: { 40 | name: 'my_counter', 41 | help: 'my_counter a simple counter', 42 | }, 43 | }, 44 | { 45 | type: MetricType.Gauge, 46 | configuration: { 47 | name: 'my_gauge', 48 | help: 'my_gauge a simple gauge', 49 | }, 50 | }, 51 | { 52 | type: MetricType.Histogram, 53 | configuration: { 54 | name: 'my_histogram', 55 | help: 'my_histogram a simple histogram', 56 | }, 57 | }, 58 | { 59 | type: MetricType.Summary, 60 | configuration: { 61 | name: 'my_summary', 62 | help: 'my_summary a simple summary', 63 | }, 64 | }, 65 | ], 66 | ), 67 | ], 68 | }) 69 | export class AppModule {} 70 | ``` 71 | 72 | ### 2. 增加 asynchronous provider 73 | 74 | ```typescript 75 | @Module({ 76 | imports: [ 77 | PromModule.forRootAsync( 78 | { 79 | useFactory(configService: ConfigService) { 80 | return { 81 | withDefaultsMetrics: configService.get(false), 82 | }; 83 | }, 84 | inject: [ConfigService], 85 | imports: [ConfigModule], 86 | }, 87 | [ 88 | { 89 | type: MetricType.Counter, 90 | configuration: { 91 | name: 'my_counter', 92 | help: 'my_counter a simple counter', 93 | }, 94 | }, 95 | { 96 | type: MetricType.Gauge, 97 | configuration: { 98 | name: 'my_gauge', 99 | help: 'my_gauge a simple gauge', 100 | }, 101 | }, 102 | { 103 | type: MetricType.Histogram, 104 | configuration: { 105 | name: 'my_histogram', 106 | help: 'my_histogram a simple histogram', 107 | }, 108 | }, 109 | { 110 | type: MetricType.Summary, 111 | configuration: { 112 | name: 'my_summary', 113 | help: 'my_summary a simple summary', 114 | }, 115 | }, 116 | ], 117 | ), 118 | ], 119 | }) 120 | export class AppModule {} 121 | ``` 122 | 123 | ### 3. 使用 124 | 125 | ```typescript 126 | import { Inject, Injectable } from '@nestjs/common'; 127 | 128 | @Injectable() 129 | class TestService { 130 | constructor( 131 | @Inject('my_counter') 132 | private readonly counter: Counter, 133 | ) {} 134 | 135 | say() { 136 | this.counter.inr(); 137 | } 138 | } 139 | ``` 140 | 141 | ## License 142 | 143 | Nest is [MIT licensed](LICENSE). 144 | -------------------------------------------------------------------------------- /packages/nestjs-prom/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2021/4/12 3 | */ 4 | export * from './lib'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/constant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/19 3 | */ 4 | // export type MetricTypeList = 'Counter' | 'Gauge' | 'Histogram' | 'Summary'; 5 | 6 | export const PROM_MODULE_OPTIONS = Symbol.for('LOGGER_MODULE_OPTIONS'); 7 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/decorator/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/22 3 | */ 4 | export * from './prom.decorator'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/decorator/prom.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/18 3 | */ 4 | import { Counter, CounterConfiguration } from 'prom-client'; 5 | 6 | export function PromMethodCounter( 7 | counterConfig: CounterConfiguration = { name: '', help: '' }, 8 | ): MethodDecorator { 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | return ( 12 | target: Record, 13 | propertyKey: string | symbol, 14 | descriptor: TypedPropertyDescriptor, 15 | ) => { 16 | const className = target.constructor.name; 17 | 18 | counterConfig.name = `app_${className}_${propertyKey.toString()}_calls_total`; 19 | counterConfig.help = `app_${className}#${propertyKey.toString()} called total`; 20 | const counterMetric = new Counter(counterConfig); 21 | 22 | const methodFunc = descriptor.value; 23 | descriptor.value = function (...args) { 24 | counterMetric.inc(1); 25 | return methodFunc.apply(this, args); 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/decorator/prom.paramter.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/19 3 | */ 4 | import { 5 | Counter, 6 | CounterConfiguration, 7 | Gauge, 8 | GaugeConfiguration, 9 | Histogram, 10 | HistogramConfiguration, 11 | Metric, 12 | Summary, 13 | SummaryConfiguration, 14 | } from 'prom-client'; 15 | 16 | const metricMetadataKey = Symbol('metrics parameter metadata key'); 17 | 18 | export function Counter1( 19 | config: CounterConfiguration, 20 | ): ParameterDecorator { 21 | return (target, propertyKey, parameterIndex) => { 22 | const metricList: { index: number; metric: Metric }[] = 23 | Reflect.getOwnMetadata(metricMetadataKey, target, propertyKey) || []; 24 | metricList.push({ index: parameterIndex, metric: new Counter(config) }); 25 | 26 | Reflect.defineMetadata(metricMetadataKey, metricList, target, propertyKey); 27 | }; 28 | } 29 | 30 | export function Gauge1( 31 | config: GaugeConfiguration, 32 | ): ParameterDecorator { 33 | return (target, propertyKey, parameterIndex) => { 34 | const metricList: { index: number; metric: Metric }[] = 35 | Reflect.getOwnMetadata(metricMetadataKey, target, propertyKey) || []; 36 | metricList.push({ index: parameterIndex, metric: new Gauge(config) }); 37 | 38 | Reflect.defineMetadata(metricMetadataKey, metricList, target, propertyKey); 39 | }; 40 | } 41 | 42 | export function Histogram1( 43 | config: HistogramConfiguration, 44 | ): ParameterDecorator { 45 | return (target, propertyKey, parameterIndex) => { 46 | const metricList: { index: number; metric: Metric }[] = 47 | Reflect.getOwnMetadata(metricMetadataKey, target, propertyKey) || []; 48 | metricList.push({ index: parameterIndex, metric: new Histogram(config) }); 49 | 50 | Reflect.defineMetadata(metricMetadataKey, metricList, target, propertyKey); 51 | }; 52 | } 53 | 54 | export function Summary1( 55 | config: SummaryConfiguration, 56 | ): ParameterDecorator { 57 | return (target, propertyKey, parameterIndex) => { 58 | const metricList: { index: number; metric: Metric }[] = 59 | Reflect.getOwnMetadata(metricMetadataKey, target, propertyKey) || []; 60 | metricList.push({ index: parameterIndex, metric: new Summary(config) }); 61 | 62 | Reflect.defineMetadata(metricMetadataKey, metricList, target, propertyKey); 63 | }; 64 | } 65 | 66 | export function MetricExecute(): MethodDecorator { 67 | return ( 68 | target, 69 | propertyKey: string | symbol, 70 | descriptor: TypedPropertyDescriptor, 71 | ) => { 72 | const method = descriptor.value; 73 | descriptor.value = function (...args) { 74 | const metrics = Reflect.getOwnMetadata( 75 | metricMetadataKey, 76 | target, 77 | propertyKey, 78 | ); 79 | for (const { index, metric } of metrics) { 80 | args[index] = metric; 81 | } 82 | 83 | return method.apply(this, args); 84 | }; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/29 3 | */ 4 | export * from './decorator'; 5 | export * from './interfaces'; 6 | 7 | export * from './constant'; 8 | export * from './prom.module'; 9 | export * from './prom-metric.provider'; 10 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/22 3 | */ 4 | export * from './metric.type'; 5 | export * from './prom-module-async-options.interface'; 6 | export * from './prom-options.interface'; 7 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/interfaces/metric.type.ts: -------------------------------------------------------------------------------- 1 | import * as PromClient from 'prom-client'; 2 | 3 | export enum MetricType { 4 | Counter, 5 | Gauge, 6 | Histogram, 7 | Summary, 8 | } 9 | 10 | export interface MetricTypeConfigurationInterface { 11 | type: MetricType; 12 | configuration?: any; 13 | } 14 | 15 | export class MetricTypeCounter implements MetricTypeConfigurationInterface { 16 | type: MetricType = MetricType.Counter; 17 | configuration: PromClient.CounterConfiguration; 18 | } 19 | 20 | export class MetricTypeGauge implements MetricTypeConfigurationInterface { 21 | type: MetricType = MetricType.Gauge; 22 | configuration: PromClient.GaugeConfiguration; 23 | } 24 | 25 | export class MetricTypeHistogram implements MetricTypeConfigurationInterface { 26 | type: MetricType = MetricType.Histogram; 27 | configuration: PromClient.HistogramConfiguration; 28 | } 29 | 30 | export class MetricTypeSummary implements MetricTypeConfigurationInterface { 31 | type: MetricType = MetricType.Summary; 32 | configuration: PromClient.SummaryConfiguration; 33 | } 34 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/interfaces/prom-module-async-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/19 3 | */ 4 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; 5 | 6 | import { PromModuleOptions } from './prom-options.interface'; 7 | 8 | export interface PromOptionsFactory { 9 | createLoggerOptions(): Promise | PromModuleOptions; 10 | } 11 | 12 | export interface LoggerModuleAsyncOptions 13 | extends Pick { 14 | name?: string; 15 | useExisting?: Type; 16 | useClass?: Type; 17 | useFactory?: ( 18 | ...args: any[] 19 | ) => Promise | PromModuleOptions; 20 | inject?: any[]; 21 | } 22 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/interfaces/prom-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PromModuleOptions { 2 | registerName?: string; 3 | 4 | /** 5 | * Enable default metrics 6 | * Under the hood, that call collectDefaultMetrics() 7 | */ 8 | withDefaultsMetrics?: boolean; 9 | 10 | defaultLabels?: { 11 | [key: string]: any; 12 | }; 13 | 14 | defaultMetricsCollectionConfig?: { 15 | prefix?: string; 16 | gcDurationBuckets?: number[]; 17 | eventLoopMonitoringPrecision?: number; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/prom-metric.provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/29 3 | */ 4 | import { Provider } from '@nestjs/common'; 5 | import { 6 | Counter, 7 | CounterConfiguration, 8 | Gauge, 9 | GaugeConfiguration, 10 | Histogram, 11 | HistogramConfiguration, 12 | Registry, 13 | Summary, 14 | SummaryConfiguration, 15 | } from 'prom-client'; 16 | 17 | export function createPromCounterProvider( 18 | configuration: CounterConfiguration, 19 | ): Provider { 20 | return { 21 | provide: configuration.name, 22 | useFactory: (registry: Registry) => { 23 | const obj = new Counter({ 24 | ...configuration, 25 | registers: [registry], 26 | }); 27 | return obj; 28 | }, 29 | inject: [Registry], 30 | }; 31 | } 32 | 33 | export function createPromGaugeProvider( 34 | configuration: GaugeConfiguration, 35 | ): Provider { 36 | return { 37 | provide: configuration.name, 38 | useFactory: (registry: Registry) => { 39 | const obj = new Gauge({ 40 | ...configuration, 41 | registers: [registry], 42 | }); 43 | return obj; 44 | }, 45 | inject: [Registry], 46 | }; 47 | } 48 | 49 | export function createPromHistogramProvider( 50 | configuration: HistogramConfiguration, 51 | ): Provider { 52 | return { 53 | provide: configuration.name, 54 | useFactory: (registry: Registry) => { 55 | const obj = new Histogram({ 56 | ...configuration, 57 | registers: [registry], 58 | }); 59 | return obj; 60 | }, 61 | inject: [Registry], 62 | }; 63 | } 64 | 65 | export function createPromSummaryProvider( 66 | configuration: SummaryConfiguration, 67 | ): Provider { 68 | return { 69 | provide: configuration.name, 70 | useFactory: (registry: Registry) => { 71 | const obj = new Summary({ 72 | ...configuration, 73 | registers: [registry], 74 | }); 75 | return obj; 76 | }, 77 | inject: [Registry], 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /packages/nestjs-prom/lib/prom.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/18 3 | */ 4 | import { 5 | DynamicModule, 6 | FactoryProvider, 7 | Global, 8 | Module, 9 | Provider, 10 | Type, 11 | ValueProvider, 12 | } from '@nestjs/common'; 13 | import * as client from 'prom-client'; 14 | import { collectDefaultMetrics, Registry } from 'prom-client'; 15 | 16 | import { PROM_MODULE_OPTIONS } from './constant'; 17 | import { 18 | LoggerModuleAsyncOptions, 19 | MetricType, 20 | MetricTypeConfigurationInterface, 21 | PromModuleOptions, 22 | PromOptionsFactory, 23 | } from './interfaces'; 24 | import { 25 | createPromCounterProvider, 26 | createPromGaugeProvider, 27 | createPromHistogramProvider, 28 | createPromSummaryProvider, 29 | } from './prom-metric.provider'; 30 | 31 | @Global() 32 | @Module({}) 33 | export class PromModule { 34 | static forRoot( 35 | options?: PromModuleOptions, 36 | metrics: MetricTypeConfigurationInterface[] = [], 37 | ): DynamicModule { 38 | options = Object.assign(PromModule.defaultOptions(), options); 39 | 40 | const { 41 | defaultLabels, 42 | defaultMetricsCollectionConfig, 43 | withDefaultsMetrics, 44 | } = options; 45 | 46 | const providers: Provider[] = []; 47 | const promOptionsProvider: ValueProvider = { 48 | provide: PROM_MODULE_OPTIONS, 49 | useValue: options, 50 | }; 51 | providers.push(promOptionsProvider); 52 | 53 | const registerProvider: FactoryProvider = { 54 | provide: Registry, 55 | useFactory: () => { 56 | const register: Registry = client.register; 57 | 58 | if (defaultLabels) { 59 | register.setDefaultLabels(defaultLabels); 60 | } 61 | 62 | if (withDefaultsMetrics === true) { 63 | collectDefaultMetrics( 64 | Object.assign( 65 | { 66 | register, 67 | }, 68 | defaultMetricsCollectionConfig, 69 | ), 70 | ); 71 | } 72 | 73 | return register; 74 | }, 75 | }; 76 | providers.push(registerProvider); 77 | 78 | const metricProvider: Provider[] = metrics.map((entry) => { 79 | switch (entry.type) { 80 | case MetricType.Counter: 81 | return createPromCounterProvider(entry.configuration); 82 | case MetricType.Gauge: 83 | return createPromGaugeProvider(entry.configuration); 84 | case MetricType.Histogram: 85 | return createPromHistogramProvider(entry.configuration); 86 | case MetricType.Summary: 87 | return createPromSummaryProvider(entry.configuration); 88 | default: 89 | throw new ReferenceError(`The type ${entry.type} is not supported`); 90 | } 91 | }); 92 | 93 | return { 94 | module: PromModule, 95 | imports: [], 96 | exports: [...providers, ...metricProvider], 97 | providers: [...providers, ...metricProvider], 98 | }; 99 | } 100 | 101 | static forRootAsync( 102 | options: LoggerModuleAsyncOptions, 103 | metrics: MetricTypeConfigurationInterface[] = [], 104 | ): DynamicModule { 105 | const providers: Provider[] = []; 106 | 107 | providers.push(...this.createAsyncProviders(options)); 108 | 109 | const registerFactory: FactoryProvider = { 110 | provide: Registry, 111 | useFactory: (_option: PromModuleOptions) => { 112 | const register = client.register; 113 | if (_option.withDefaultsMetrics === true) { 114 | collectDefaultMetrics(); 115 | } 116 | 117 | if (_option.defaultLabels) { 118 | register.setDefaultLabels(_option.defaultLabels); 119 | } 120 | return register; 121 | }, 122 | inject: [PROM_MODULE_OPTIONS], 123 | }; 124 | providers.push(registerFactory); 125 | 126 | const metricProvider: Provider[] = metrics.map((entry) => { 127 | switch (entry.type) { 128 | case MetricType.Counter: 129 | return createPromCounterProvider(entry.configuration); 130 | case MetricType.Gauge: 131 | return createPromGaugeProvider(entry.configuration); 132 | case MetricType.Histogram: 133 | return createPromHistogramProvider(entry.configuration); 134 | case MetricType.Summary: 135 | return createPromSummaryProvider(entry.configuration); 136 | default: 137 | throw new ReferenceError(`The type ${entry.type} is not supported`); 138 | } 139 | }); 140 | 141 | return { 142 | module: PromModule, 143 | imports: options.imports, 144 | providers: [...providers, ...metricProvider], 145 | exports: [...providers, ...metricProvider], 146 | }; 147 | } 148 | 149 | private static createAsyncProviders( 150 | options: LoggerModuleAsyncOptions, 151 | ): Provider[] { 152 | if (options.useExisting || options.useFactory) { 153 | return [this.createAsyncOptionsProvider(options)]; 154 | } 155 | const useClass = options.useClass as Type; 156 | return [ 157 | this.createAsyncOptionsProvider(options), 158 | { 159 | provide: useClass, 160 | useClass, 161 | }, 162 | ]; 163 | } 164 | 165 | private static createAsyncOptionsProvider( 166 | options: LoggerModuleAsyncOptions, 167 | ): Provider { 168 | if (options.useFactory) { 169 | return { 170 | provide: PROM_MODULE_OPTIONS, 171 | useFactory: options.useFactory, 172 | inject: options.inject || [], 173 | }; 174 | } 175 | 176 | const inject = [ 177 | (options.useClass || options.useExisting) as Type, 178 | ]; 179 | return { 180 | provide: PROM_MODULE_OPTIONS, 181 | useFactory: async (optionsFactory: PromOptionsFactory) => 182 | await optionsFactory.createLoggerOptions(), 183 | inject, 184 | }; 185 | } 186 | 187 | private static defaultOptions(): PromModuleOptions { 188 | return { 189 | withDefaultsMetrics: true, 190 | defaultMetricsCollectionConfig: {}, 191 | }; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /packages/nestjs-prom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@donews/nestjs-prom", 3 | "version": "0.11.1", 4 | "description": "nestjs prom module", 5 | "author": "toonewLi ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": {}, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/DoNewsCode/nestjs.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/DoNewsCode/nestjs/issues" 18 | }, 19 | "dependencies": { 20 | "prom-client": "^15.0.0" 21 | }, 22 | "devDependencies": { 23 | "@nestjs/common": "11.1.3", 24 | "@nestjs/core": "11.1.3", 25 | "@nestjs/platform-express": "11.1.3", 26 | "@nestjs/testing": "11.1.3" 27 | }, 28 | "peerDependencies": { 29 | "@nestjs/common": "^11.0.0", 30 | "@nestjs/core": "^11.0.0" 31 | }, 32 | "gitHead": "4e0a8578a08c92cf2229122d58940c204034ba7b" 33 | } 34 | -------------------------------------------------------------------------------- /packages/nestjs-prom/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-prom/test/prom.async.module.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/18 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { Injectable, Module } from '@nestjs/common'; 7 | import { Test, TestingModule } from '@nestjs/testing'; 8 | import { Counter, Gauge, register, Registry } from 'prom-client'; 9 | 10 | import { 11 | MetricType, 12 | MetricTypeCounter, 13 | MetricTypeGauge, 14 | PromModule, 15 | } from '../lib'; 16 | 17 | @Injectable() 18 | class ConfigService { 19 | get(value: any) { 20 | return value; 21 | } 22 | } 23 | 24 | @Module({ exports: [ConfigService], providers: [ConfigService] }) 25 | class ConfigModule {} 26 | 27 | describe('Prom Async Module', () => { 28 | beforeEach(() => { 29 | register.clear(); 30 | }); 31 | 32 | it('collection default is false', async () => { 33 | const module: TestingModule = await Test.createTestingModule({ 34 | imports: [ 35 | PromModule.forRootAsync({ 36 | useFactory(configService: ConfigService) { 37 | return { 38 | withDefaultsMetrics: configService.get(false), 39 | }; 40 | }, 41 | inject: [ConfigService], 42 | imports: [ConfigModule], 43 | }), 44 | ], 45 | }).compile(); 46 | 47 | const register1 = module.get(Registry); 48 | 49 | const actual = await register1.metrics(); 50 | assert.strictEqual(actual, '\n'); 51 | }); 52 | 53 | it('collection default is true', async () => { 54 | const module: TestingModule = await Test.createTestingModule({ 55 | imports: [ 56 | PromModule.forRootAsync({ 57 | useFactory(configService: ConfigService) { 58 | return { 59 | withDefaultsMetrics: configService.get(true), 60 | }; 61 | }, 62 | inject: [ConfigService], 63 | imports: [ConfigModule], 64 | }), 65 | ], 66 | }).compile(); 67 | 68 | const register1 = module.get(Registry); 69 | 70 | assert.strictEqual((await register1.metrics()) !== '', true); 71 | }); 72 | 73 | it('default labels', async () => { 74 | const module: TestingModule = await Test.createTestingModule({ 75 | imports: [ 76 | PromModule.forRootAsync( 77 | { 78 | useFactory(configService: ConfigService) { 79 | return { 80 | withDefaultsMetrics: configService.get(false), 81 | defaultLabels: { env: 'testing' }, 82 | }; 83 | }, 84 | inject: [ConfigService], 85 | imports: [ConfigModule], 86 | }, 87 | [ 88 | { 89 | type: MetricType.Counter, 90 | configuration: { 91 | name: 'test_count', 92 | help: 'test count help', 93 | labelNames: ['field1', 'field2'], 94 | }, 95 | } as MetricTypeCounter, 96 | ], 97 | ), 98 | ], 99 | }).compile(); 100 | 101 | const register1 = module.get(Registry); 102 | const counter = module.get>('test_count'); 103 | 104 | counter.labels('value1', 'value2').inc(1); 105 | 106 | const expectValue = `# HELP test_count test count help 107 | # TYPE test_count counter 108 | test_count{field1="value1",field2="value2",env="testing"} 1`; 109 | 110 | assert.strictEqual( 111 | await register1.getSingleMetricAsString('test_count'), 112 | expectValue, 113 | ); 114 | }); 115 | 116 | it('counter', async () => { 117 | const module: TestingModule = await Test.createTestingModule({ 118 | imports: [ 119 | PromModule.forRootAsync( 120 | { 121 | useFactory(configService: ConfigService) { 122 | return { 123 | withDefaultsMetrics: configService.get(true), 124 | }; 125 | }, 126 | inject: [ConfigService], 127 | imports: [ConfigModule], 128 | }, 129 | [ 130 | { 131 | type: MetricType.Counter, 132 | configuration: { 133 | name: 'test_count', 134 | help: 'test count help', 135 | labelNames: ['field1', 'field2'], 136 | }, 137 | } as MetricTypeCounter, 138 | ], 139 | ), 140 | ], 141 | }).compile(); 142 | 143 | const register1 = module.get(Registry); 144 | const counter = module.get>('test_count'); 145 | 146 | counter.labels('value1', 'value2').inc(1); 147 | 148 | const expectValue = `# HELP test_count test count help 149 | # TYPE test_count counter 150 | test_count{field1="value1",field2="value2"} 1`; 151 | 152 | assert.strictEqual( 153 | await register1.getSingleMetricAsString('test_count'), 154 | expectValue, 155 | ); 156 | }); 157 | 158 | it('gauge', async () => { 159 | const module: TestingModule = await Test.createTestingModule({ 160 | imports: [ 161 | PromModule.forRootAsync( 162 | { 163 | useFactory(configService: ConfigService) { 164 | return { 165 | withDefaultsMetrics: configService.get(true), 166 | }; 167 | }, 168 | inject: [ConfigService], 169 | imports: [ConfigModule], 170 | }, 171 | [ 172 | { 173 | type: MetricType.Gauge, 174 | configuration: { 175 | name: 'test_gauge', 176 | help: 'test gauge help', 177 | labelNames: ['field1', 'field2'], 178 | }, 179 | } as MetricTypeGauge, 180 | ], 181 | ), 182 | ], 183 | }).compile(); 184 | 185 | const register1 = module.get(Registry); 186 | const counter = module.get>('test_gauge'); 187 | 188 | counter.labels('value1', 'value2').set(10); 189 | 190 | const expectValue = `# HELP test_gauge test gauge help 191 | # TYPE test_gauge gauge 192 | test_gauge{field1="value1",field2="value2"} 10`; 193 | 194 | assert.strictEqual( 195 | await register1.getSingleMetricAsString('test_gauge'), 196 | expectValue, 197 | ); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /packages/nestjs-prom/test/prom.module.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/6/18 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { Test, TestingModule } from '@nestjs/testing'; 7 | import { Counter, Gauge, register, Registry } from 'prom-client'; 8 | 9 | import { 10 | MetricType, 11 | MetricTypeCounter, 12 | MetricTypeGauge, 13 | PromModule, 14 | } from '../lib'; 15 | 16 | describe('PromController', () => { 17 | beforeEach(() => { 18 | register.clear(); 19 | }); 20 | 21 | it('collection default is false', async () => { 22 | const module: TestingModule = await Test.createTestingModule({ 23 | imports: [ 24 | PromModule.forRoot({ 25 | withDefaultsMetrics: false, 26 | }), 27 | ], 28 | }).compile(); 29 | 30 | const register1 = module.get(Registry); 31 | 32 | const actual = await register1.metrics(); 33 | assert.strictEqual(actual, '\n'); 34 | }); 35 | 36 | it('collection default is true', async () => { 37 | const module: TestingModule = await Test.createTestingModule({ 38 | imports: [PromModule.forRoot({})], 39 | }).compile(); 40 | 41 | const register1 = module.get(Registry); 42 | 43 | assert.strictEqual((await register1.metrics()) !== '', true); 44 | }); 45 | 46 | it('default labels', async () => { 47 | const module: TestingModule = await Test.createTestingModule({ 48 | imports: [ 49 | PromModule.forRoot( 50 | { 51 | withDefaultsMetrics: false, 52 | defaultLabels: { env: 'testing' }, 53 | }, 54 | [ 55 | { 56 | type: MetricType.Counter, 57 | configuration: { 58 | name: 'test_count', 59 | help: 'test count help', 60 | labelNames: ['field1', 'field2'], 61 | }, 62 | } as MetricTypeCounter, 63 | ], 64 | ), 65 | ], 66 | }).compile(); 67 | 68 | const register1 = module.get(Registry); 69 | const counter = module.get>('test_count'); 70 | 71 | counter.labels('value1', 'value2').inc(1); 72 | 73 | const expectValue = `# HELP test_count test count help 74 | # TYPE test_count counter 75 | test_count{field1="value1",field2="value2",env="testing"} 1`; 76 | 77 | assert.strictEqual( 78 | await register1.getSingleMetricAsString('test_count'), 79 | expectValue, 80 | ); 81 | }); 82 | 83 | it('counter', async () => { 84 | const module: TestingModule = await Test.createTestingModule({ 85 | imports: [ 86 | PromModule.forRoot({}, [ 87 | { 88 | type: MetricType.Counter, 89 | configuration: { 90 | name: 'test_count', 91 | help: 'test count help', 92 | labelNames: ['field1', 'field2'], 93 | }, 94 | } as MetricTypeCounter, 95 | ]), 96 | ], 97 | }).compile(); 98 | 99 | const register1 = module.get(Registry); 100 | const counter = module.get>('test_count'); 101 | 102 | counter.labels('value1', 'value2').inc(1); 103 | 104 | const expectValue = `# HELP test_count test count help 105 | # TYPE test_count counter 106 | test_count{field1="value1",field2="value2"} 1`; 107 | 108 | assert.strictEqual( 109 | await register1.getSingleMetricAsString('test_count'), 110 | expectValue, 111 | ); 112 | }); 113 | 114 | it('gauge', async () => { 115 | const module: TestingModule = await Test.createTestingModule({ 116 | imports: [ 117 | PromModule.forRoot({}, [ 118 | { 119 | type: MetricType.Gauge, 120 | configuration: { 121 | name: 'test_gauge', 122 | help: 'test gauge help', 123 | labelNames: ['field1', 'field2'], 124 | }, 125 | } as MetricTypeGauge, 126 | ]), 127 | ], 128 | }).compile(); 129 | 130 | const register1 = module.get(Registry); 131 | const counter = module.get>('test_gauge'); 132 | 133 | counter.labels('value1', 'value2').set(10); 134 | 135 | const expectValue = `# HELP test_gauge test gauge help 136 | # TYPE test_gauge gauge 137 | test_gauge{field1="value1",field2="value2"} 10`; 138 | 139 | assert.strictEqual( 140 | await register1.getSingleMetricAsString('test_gauge'), 141 | expectValue, 142 | ); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /packages/nestjs-prom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/README.md: -------------------------------------------------------------------------------- 1 | # nest open tracing 2 | 3 | ## 描述 4 | 5 | nest http tracing 工具 6 | 7 | ## 待支持 8 | 9 | 1. websocket 10 | 11 | ## install 12 | 13 | ```shell 14 | $ npm i @donews/nestjs-tracing jaeger-client opentracing 15 | $ npm i -D @types/jaeger-client 16 | ``` 17 | 18 | ## 参数解释 19 | 20 | - tracingConfig&tracingOption: [jaeger-client](https://github.com/jaegertracing/jaeger-client-node) 21 | - tracingOption 增加 contextKey,baggagePrefix,自定义 tracingId Key 字段,baggage 前缀字段 22 | 23 | ## quick start 24 | 25 | ### 1. 同步模块导入 26 | 27 | ```typescript 28 | import { TracingModule } from '@donews/nestjs-tracing'; 29 | import { Module } from '@nestjs/common'; 30 | 31 | @Module({ 32 | imports: [ 33 | TracingModule.forRoot({ 34 | tracingConfig: { 35 | serviceName: 'tracing-service', 36 | sampler: { 37 | param: 1, 38 | type: 'const', 39 | }, 40 | }, 41 | tracingOption: { 42 | tags: {}, 43 | }, 44 | }), 45 | ], 46 | }) 47 | export class AppModule {} 48 | ``` 49 | 50 | ### 2. 异步模块导入 51 | 52 | ```typescript 53 | import { TracingModule, TracingModuleOptions } from '@donews/nestjs-tracing'; 54 | import { Module } from '@nestjs/common'; 55 | 56 | @Module({ 57 | imports: [ 58 | TracingModule.forRootAsync({ 59 | useFactory(configService: ConfigService) { 60 | return { 61 | tracingConfig: { 62 | serviceName: configService.get('APP_NAME'), 63 | sampler: { 64 | param: 1, 65 | type: 'const', 66 | }, 67 | }, 68 | tracingOption: { 69 | tags: {}, 70 | }, 71 | } as TracingModuleOptions; 72 | }, 73 | inject: [ConfigService], 74 | }), 75 | ], 76 | }) 77 | export class AppModule {} 78 | ``` 79 | 80 | ### 3. http server receive request interceptor 81 | 82 | HttpModule 上的 HttpService 设置 interceptors 83 | 84 | ```typescript 85 | import { HttpServiceInterceptors } from '@donews/nestjs-tracing/lib/http-service.interceptors'; 86 | import { HttpModule, HttpService } from '@nestjs/axios'; 87 | 88 | @Module({}) 89 | export class AdModule implements OnApplicationBootstrap { 90 | constructor(private readonly httpService: HttpService) {} 91 | /** 92 | * 设置axios拦截器,增加tracing信息 93 | */ 94 | onApplicationBootstrap(): void { 95 | this.httpService.axiosRef.interceptors.request.use( 96 | ...HttpServiceInterceptors.interceptRequest(), 97 | ); 98 | this.httpService.axiosRef.interceptors.response.use( 99 | ...HttpServiceInterceptors.interceptResponse(), 100 | ); 101 | } 102 | } 103 | ``` 104 | 105 | http-request-tracing 会自动读取 header 和 query 上的 tracingId,如果自定义了 tracingId key 值,请在模块导入 tracingOption 进行修改 106 | 107 | ```typescript 108 | import { TracingHttpInterceptor } from '@donews/nestjs-tracing'; 109 | import { UseInterceptors } from '@nestjs/common'; 110 | 111 | @UseInterceptors(TracingHttpInterceptor) 112 | export class AppController {} 113 | ``` 114 | 115 | ### 4. grpc client request interceptor 116 | 117 | ```typescript 118 | import { tracingGrpcInterceptor } from '@donews/nestjs-tracing'; 119 | import { ClientProxyFactory } from '@nestjs/microservices'; 120 | import { FactoryProvider } from '@nestjs/common'; 121 | 122 | export const RawGrpcProvider: FactoryProvider = { 123 | provide: 'RAW_SERVICE', 124 | useFactory: () => { 125 | return ClientProxyFactory.create({ 126 | transport: Transport.GRPC, 127 | options: { 128 | channelOptions: { 129 | interceptors: [tracingGrpcInterceptor] as any, 130 | }, 131 | }, 132 | }); 133 | }, 134 | }; 135 | ``` 136 | 137 | [online example](https://github.com/DoNewsCode/nestjs-tracing/blob/master/sample/async-hook/proto/raw.service.ts) 138 | 139 | ## License 140 | 141 | Nest is [MIT licensed](LICENSE). 142 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/aop/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2021/6/7 3 | */ 4 | export * from './tracing-http.interceptor'; 5 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/aop/tracing-grpc.interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | import { InterceptingCall, Listener, Metadata, Requester } from '@grpc/grpc-js'; 5 | 6 | import { TRACER_CARRIER_INFO } from '../constant'; 7 | import { AsyncContext } from '../hook'; 8 | 9 | export function tracingGrpcInterceptor( 10 | options: any, 11 | nextCall: (...args: any) => any, 12 | ): InterceptingCall { 13 | const request: Requester = { 14 | start(metadata: Metadata, listener: Listener, next: any): void { 15 | const carrier = AsyncContext.getInstance().get(TRACER_CARRIER_INFO); 16 | for (const key of Object.keys(carrier)) { 17 | metadata.add(key, carrier[key]); 18 | } 19 | next(metadata, listener); 20 | }, 21 | }; 22 | return new InterceptingCall(nextCall(options), request); 23 | } 24 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/aop/tracing-http.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { Request, Response } from 'express'; 9 | import { 10 | FORMAT_HTTP_HEADERS, 11 | FORMAT_TEXT_MAP, 12 | Span, 13 | SpanContext, 14 | Tags, 15 | Tracer, 16 | } from 'opentracing'; 17 | import { Observable, throwError } from 'rxjs'; 18 | import { catchError, tap } from 'rxjs/operators'; 19 | 20 | import { 21 | TRACER, 22 | TRACER_BAGGAGE_HEADER_PREFIX, 23 | TRACER_CARRIER_INFO, 24 | TRACER_STATE_HEADER_NAME, 25 | TRACING_MODULE_OPTIONS, 26 | } from '../constant'; 27 | import { AsyncContext } from '../hook'; 28 | import { TracingModuleOptions } from '../interface/tracing.interface'; 29 | import { isSpanContext } from '../util/span.util'; 30 | 31 | /** 32 | * Created by Rain on 2020/7/21 33 | */ 34 | @Injectable() 35 | export class TracingHttpInterceptor implements NestInterceptor { 36 | constructor( 37 | private readonly asyncContext: AsyncContext, 38 | @Inject(TRACER) private readonly tracer: Tracer, 39 | @Inject(TRACING_MODULE_OPTIONS) 40 | private readonly tracingModuleOptions: TracingModuleOptions, 41 | ) {} 42 | 43 | intercept( 44 | context: ExecutionContext, 45 | next: CallHandler, 46 | ): Observable | Promise> { 47 | const request: Request = context.switchToHttp().getRequest(); 48 | const response: Response = context.switchToHttp().getResponse(); 49 | const handler = context.getHandler().name; 50 | const controller = context.getClass().name; 51 | 52 | const tracingContext = this.getTracingContext(request); 53 | 54 | const carrier: any = tracingContext?.carrier ? tracingContext.carrier : {}; 55 | 56 | let span: Span; 57 | if (tracingContext?.spanContext) { 58 | span = this.tracer.startSpan(`${controller}.${handler}`, { 59 | childOf: tracingContext.spanContext, 60 | }); 61 | } else { 62 | span = this.tracer.startSpan(`${controller}.${handler}`); 63 | } 64 | 65 | return this.asyncContext.run(() => { 66 | this.tracer.inject(span, FORMAT_TEXT_MAP, carrier); 67 | this.asyncContext.set(TRACER_CARRIER_INFO, carrier); 68 | 69 | span.setTag('path', request.path); 70 | span.setTag('method', request.method); 71 | span.log({ url: request.url }); 72 | 73 | return next.handle().pipe( 74 | tap(() => { 75 | span.finish(); 76 | }), 77 | catchError((error: any) => { 78 | span.setTag(Tags.ERROR, true); 79 | span.log({ 80 | 'err.stack': error.stack, 81 | statusCode: response.statusCode, 82 | }); 83 | span.finish(); 84 | return throwError(error); 85 | }), 86 | ); 87 | }); 88 | } 89 | 90 | private getTracingContext( 91 | req: Request, 92 | ): null | { carrier: any; spanContext: SpanContext } { 93 | const carrier = this.getTracingCarrier([req.headers, req.query]); 94 | const spanContext = this.tracer.extract(FORMAT_HTTP_HEADERS, carrier); 95 | 96 | if (isSpanContext(spanContext)) { 97 | return { carrier, spanContext }; 98 | } 99 | return null; 100 | } 101 | 102 | getTracingCarrier(carrierList: Record[]): Record { 103 | const carrierEnd: Record = {}; 104 | 105 | const { tracingOption } = this.tracingModuleOptions; 106 | 107 | const contextKey = tracingOption?.contextKey ?? TRACER_STATE_HEADER_NAME; 108 | const baggagePrefix = 109 | tracingOption?.baggagePrefix ?? TRACER_BAGGAGE_HEADER_PREFIX; 110 | 111 | for (const carrier of carrierList) { 112 | const keys = Object.keys(carrier); 113 | for (const key of keys) { 114 | const lowKey = key.toLowerCase(); 115 | if (lowKey === contextKey) { 116 | carrierEnd[key] = carrier[key]; 117 | } 118 | if (lowKey.startsWith(baggagePrefix)) { 119 | carrierEnd[key] = carrier[key]; 120 | } 121 | } 122 | } 123 | 124 | return carrierEnd; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/constant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | export const TRACING_MODULE_OPTIONS = Symbol('TRACING_MODULE_OPTIONS'); 5 | 6 | export const TRACER = Symbol('tracerProvider'); 7 | 8 | // execute stack 存储信息载体 9 | export const TRACER_CARRIER_INFO = Symbol('tracingInfo'); 10 | 11 | // TRACER_BAGGAGE_HEADER_PREFIX is the default prefix used for saving baggage to a carrier. 12 | export const TRACER_BAGGAGE_HEADER_PREFIX = 'uberctx-'; 13 | 14 | // TRACER_STATE_HEADER_NAME is the header key used for a span's serialized context. 15 | export const TRACER_STATE_HEADER_NAME = 'uber-trace-id'; 16 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/decorator/spand.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/19 3 | */ 4 | import { FORMAT_TEXT_MAP, Span, Tags } from 'opentracing'; 5 | 6 | import { TRACER_CARRIER_INFO } from '../constant'; 7 | import { AsyncContext } from '../hook'; 8 | import { TracingModule } from '../tracing.module'; 9 | 10 | export function SpanD(name: string): MethodDecorator { 11 | return ( 12 | target: any, 13 | propertyKey: string | symbol, 14 | descriptor: TypedPropertyDescriptor, 15 | ) => { 16 | const original = descriptor.value; 17 | 18 | descriptor.value = function (...args: any[]) { 19 | let context: any; 20 | try { 21 | context = AsyncContext.getInstance().get(TRACER_CARRIER_INFO); 22 | } catch (err) { 23 | return original.apply(this, args); 24 | } 25 | 26 | if (!context) { 27 | context = {}; 28 | AsyncContext.getInstance().set(TRACER_CARRIER_INFO, context); 29 | } 30 | 31 | const tracer = TracingModule.tracer; 32 | const ctx = tracer.extract(FORMAT_TEXT_MAP, context); 33 | 34 | let span: Span; 35 | if (ctx) { 36 | span = tracer.startSpan(name, { childOf: ctx }); 37 | } else { 38 | span = tracer.startSpan(name); 39 | } 40 | 41 | const result = original.apply(this, args); 42 | 43 | if (result?.then) { 44 | result 45 | .then(() => { 46 | tracer.inject(span, FORMAT_TEXT_MAP, context); 47 | span.finish(); 48 | }) 49 | .catch(() => { 50 | tracer.inject(span, FORMAT_TEXT_MAP, context); 51 | span.setTag(Tags.ERROR, true); 52 | span.finish(); 53 | }); 54 | } else { 55 | span.finish(); 56 | } 57 | 58 | return result; 59 | }; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | import * as asyncHooks from 'async_hooks'; 5 | 6 | import { OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 7 | 8 | import { AsyncHooksHelper } from './async-hooks-helper'; 9 | import { AsyncHooksStorage } from './async-hooks-storage'; 10 | 11 | export class AsyncContext implements OnModuleInit, OnModuleDestroy { 12 | private static _instance: AsyncContext; 13 | 14 | private constructor( 15 | private readonly internalStorage: Map, 16 | private readonly asyncHookRef: asyncHooks.AsyncHook, 17 | ) {} 18 | 19 | static getInstance(): AsyncContext { 20 | if (!this._instance) { 21 | this.initialize(); 22 | } 23 | return this._instance; 24 | } 25 | 26 | onModuleInit(): void { 27 | this.asyncHookRef.enable(); 28 | } 29 | 30 | onModuleDestroy(): void { 31 | this.asyncHookRef.disable(); 32 | } 33 | 34 | set(key: TKey, value: TValue): void { 35 | const store = this.getAsyncStorage(); 36 | store.set(key, value); 37 | } 38 | 39 | get(key: TKey): TReturnValue { 40 | const store = this.getAsyncStorage(); 41 | return store.get(key) as TReturnValue; 42 | } 43 | 44 | run(fn: (...args: any) => any): any { 45 | const eid = asyncHooks.executionAsyncId(); 46 | this.internalStorage.set(eid, new Map()); 47 | return fn(); 48 | } 49 | 50 | private getAsyncStorage(): Map { 51 | const eid = asyncHooks.executionAsyncId(); 52 | const state = this.internalStorage.get(eid); 53 | if (!state) { 54 | throw new Error( 55 | `Async ID (${eid}) is not registered within internal cache.`, 56 | ); 57 | } 58 | return state; 59 | } 60 | 61 | private static initialize() { 62 | const asyncHooksStorage = new AsyncHooksStorage(); 63 | const asyncHook = AsyncHooksHelper.createHooks(asyncHooksStorage); 64 | const storage = asyncHooksStorage.getInternalStorage(); 65 | 66 | this._instance = new AsyncContext(storage, asyncHook); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-hooks-helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | import * as asyncHooks from 'async_hooks'; 5 | 6 | import { AsyncHooksStorage } from './async-hooks-storage'; 7 | 8 | export class AsyncHooksHelper { 9 | static createHooks(storage: AsyncHooksStorage): asyncHooks.AsyncHook { 10 | function init( 11 | asyncId: number, 12 | type: string, 13 | triggerId: number, 14 | resource: any, 15 | ) { 16 | if (storage.has(triggerId)) { 17 | storage.inherit(asyncId, triggerId); 18 | } 19 | } 20 | 21 | function destroy(asyncId: number) { 22 | storage.delete(asyncId); 23 | } 24 | 25 | return asyncHooks.createHook({ 26 | init, 27 | destroy, 28 | } as asyncHooks.HookCallbacks); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-hooks-interceptor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | import { 5 | CallHandler, 6 | ExecutionContext, 7 | Injectable, 8 | NestInterceptor, 9 | } from '@nestjs/common'; 10 | import { Observable } from 'rxjs'; 11 | 12 | import { AsyncContext } from './async-context'; 13 | 14 | @Injectable() 15 | export class AsyncHooksInterceptor implements NestInterceptor { 16 | constructor(private readonly asyncContext: AsyncContext) {} 17 | 18 | intercept( 19 | context: ExecutionContext, 20 | next: CallHandler, 21 | ): Observable | Promise> { 22 | return this.asyncContext.run(() => next.handle()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-hooks-middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | import { Injectable, NestMiddleware } from '@nestjs/common'; 5 | import { Request, Response } from 'express'; 6 | 7 | import { AsyncContext } from './async-context'; 8 | 9 | @Injectable() 10 | export class AsyncHooksMiddleware implements NestMiddleware { 11 | constructor(private readonly asyncContext: AsyncContext) {} 12 | 13 | use(req: Request, res: Response, next: () => void): void { 14 | this.asyncContext.run(next); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-hooks-module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | import { Global, Module } from '@nestjs/common'; 5 | 6 | import { AsyncContext } from './async-context'; 7 | 8 | @Global() 9 | @Module({ 10 | providers: [ 11 | { 12 | provide: AsyncContext, 13 | useValue: AsyncContext.getInstance(), 14 | }, 15 | ], 16 | exports: [AsyncContext], 17 | }) 18 | export class AsyncHooksModule {} 19 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/async-hooks-storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | export class AsyncHooksStorage { 5 | constructor(private readonly asyncStorage = new Map()) { 6 | this.initialize(); 7 | } 8 | 9 | get(triggerId: number): T { 10 | return this.asyncStorage.get(triggerId) as T; 11 | } 12 | 13 | has(triggerId: number): boolean { 14 | return this.asyncStorage.has(triggerId); 15 | } 16 | 17 | inherit(asyncId: number, triggerId: number): void { 18 | const value = this.asyncStorage.get(triggerId); 19 | this.asyncStorage.set(asyncId, value); 20 | } 21 | 22 | delete(asyncId: number): void { 23 | this.asyncStorage.delete(asyncId); 24 | } 25 | 26 | getInternalStorage(): Map { 27 | return this.asyncStorage; 28 | } 29 | 30 | private initialize() { 31 | const initialAsyncId = 1; 32 | this.asyncStorage.set(initialAsyncId, new Map()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/hook/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | export * from './async-context'; 5 | export * from './async-hooks-middleware'; 6 | export * from './async-hooks-module'; 7 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/http-service.interceptors.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import { FORMAT_HTTP_HEADERS, FORMAT_TEXT_MAP, Span, Tags } from 'opentracing'; 3 | 4 | import { TRACER_CARRIER_INFO } from './constant'; 5 | import { AsyncContext } from './hook'; 6 | import { TracingModule } from './tracing.module'; 7 | 8 | /** 9 | * Created by Rain on 2020/7/21 10 | */ 11 | export class HttpServiceInterceptors { 12 | static interceptRequest(): any { 13 | return [ 14 | (config: AxiosRequestConfig): AxiosRequestConfig => { 15 | const tracer = TracingModule.tracer; 16 | 17 | const asyncContext = AsyncContext.getInstance(); 18 | try { 19 | let context = asyncContext.get(TRACER_CARRIER_INFO); 20 | if (!context) { 21 | context = {}; 22 | asyncContext.set(TRACER_CARRIER_INFO, context); 23 | } 24 | 25 | const ctx = tracer.extract(FORMAT_TEXT_MAP, context); 26 | 27 | let span: Span; 28 | if (ctx) { 29 | span = tracer.startSpan('http', { childOf: ctx }); 30 | } else { 31 | span = tracer.startSpan('http'); 32 | } 33 | span.addTags({ url: config.url, method: config.method }); 34 | 35 | tracer.inject(span, FORMAT_HTTP_HEADERS, config.headers); 36 | 37 | const spanConfig: any = config; 38 | spanConfig.span = span; 39 | return spanConfig as AxiosRequestConfig; 40 | } catch (err) { 41 | return config; 42 | } 43 | }, 44 | (error: any): Promise => Promise.reject(error), 45 | ]; 46 | } 47 | 48 | static interceptResponse(): any { 49 | return [ 50 | (response: AxiosResponse): any => { 51 | const config = response.config as AxiosRequestConfig & { span: Span }; 52 | if (config?.span) { 53 | config.span.log({ 54 | result: response.data, 55 | statusCode: response.status, 56 | }); 57 | config.span.finish(); 58 | } 59 | return response; 60 | }, 61 | (error: any): Promise => { 62 | const config = error.config as AxiosRequestConfig & { span: Span }; 63 | if (config?.span) { 64 | config.span.log({ 65 | result: error.response?.data, 66 | statusCode: error.response?.status, 67 | }); 68 | config.span.setTag(Tags.ERROR, true); 69 | config.span.finish(); 70 | } 71 | return Promise.reject(error); 72 | }, 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | export * from './decorator/spand.decorator'; 5 | export * from './hook/index'; 6 | export * from './interface/tracing.interface'; 7 | 8 | export * from './constant'; 9 | 10 | export * from './tracing.module'; 11 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/interface/tracing.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | import { ModuleMetadata, Type } from '@nestjs/common'; 5 | import { TracingConfig, TracingOptions } from 'jaeger-client'; 6 | 7 | export interface TracingModuleOptions { 8 | tracingConfig: TracingConfig; 9 | tracingOption: TracingOptions & { 10 | contextKey?: string; 11 | baggagePrefix?: string; 12 | }; 13 | } 14 | 15 | export interface TracingOptionsFactory { 16 | createTracingOptions(): Promise | TracingModuleOptions; 17 | } 18 | 19 | export interface TracingModuleAsyncOptions 20 | extends Pick { 21 | name?: string; 22 | useExisting?: Type; 23 | useClass?: Type; 24 | useFactory?: ( 25 | ...args: any[] 26 | ) => Promise | TracingModuleOptions; 27 | inject?: any[]; 28 | } 29 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/tracing.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicModule, 3 | FactoryProvider, 4 | Global, 5 | Module, 6 | OnApplicationShutdown, 7 | Provider, 8 | Type, 9 | } from '@nestjs/common'; 10 | import { initTracer, TracingConfig, TracingOptions } from 'jaeger-client'; 11 | import { Tracer } from 'opentracing'; 12 | 13 | import { TRACER, TRACING_MODULE_OPTIONS } from './constant'; 14 | import { AsyncHooksModule } from './hook'; 15 | import { 16 | TracingModuleAsyncOptions, 17 | TracingModuleOptions, 18 | TracingOptionsFactory, 19 | } from './interface/tracing.interface'; 20 | import { formatTracingModuleOptions } from './util/tracing-module-options.util'; 21 | 22 | /** 23 | * Created by Rain on 2020/7/16 24 | */ 25 | @Global() 26 | @Module({ 27 | imports: [AsyncHooksModule], 28 | providers: [], 29 | exports: [], 30 | }) 31 | export class TracingModule implements OnApplicationShutdown { 32 | static tracer: Tracer; 33 | 34 | static forRoot(option: { 35 | tracingConfig: TracingConfig; 36 | tracingOption: TracingOptions & { 37 | contextKey?: string; 38 | baggagePrefix?: string; 39 | }; 40 | }): DynamicModule { 41 | const { tracingConfig, tracingOption } = formatTracingModuleOptions(option); 42 | 43 | const providers: Provider[] = []; 44 | const tracerModuleOption: Provider = { 45 | provide: TRACING_MODULE_OPTIONS, 46 | useValue: { tracingConfig, tracingOption }, 47 | }; 48 | providers.push(tracerModuleOption); 49 | 50 | const tracerProvider: Provider = { 51 | provide: TRACER, 52 | useFactory() { 53 | const tracer: Tracer = initTracer(tracingConfig, tracingOption); 54 | TracingModule.tracer = tracer; 55 | return tracer; 56 | }, 57 | }; 58 | providers.push(tracerProvider); 59 | 60 | return { 61 | imports: [], 62 | module: TracingModule, 63 | providers, 64 | exports: [...providers], 65 | }; 66 | } 67 | 68 | static forRootAsync(options: TracingModuleAsyncOptions): DynamicModule { 69 | const providers: Provider[] = []; 70 | 71 | const TracerServiceProvider: FactoryProvider = { 72 | provide: TRACER, 73 | useFactory: (_options: TracingModuleOptions) => { 74 | const { tracingConfig, tracingOption } = 75 | formatTracingModuleOptions(_options); 76 | 77 | const tracer: Tracer = initTracer(tracingConfig, tracingOption); 78 | TracingModule.tracer = tracer; 79 | return tracer; 80 | }, 81 | inject: [TRACING_MODULE_OPTIONS], 82 | }; 83 | providers.push(TracerServiceProvider); 84 | 85 | providers.push(...TracingModule.createAsyncProviders(options)); 86 | 87 | return { 88 | imports: [], 89 | module: TracingModule, 90 | providers, 91 | exports: [...providers], 92 | }; 93 | } 94 | 95 | private static createAsyncProviders( 96 | options: TracingModuleAsyncOptions, 97 | ): Provider[] { 98 | if (options.useExisting || options.useFactory) { 99 | return [this.createAsyncOptionsProvider(options)]; 100 | } 101 | const useClass = options.useClass as Type; 102 | return [ 103 | this.createAsyncOptionsProvider(options), 104 | { 105 | provide: useClass, 106 | useClass, 107 | }, 108 | ]; 109 | } 110 | 111 | private static createAsyncOptionsProvider( 112 | options: TracingModuleAsyncOptions, 113 | ): Provider { 114 | if (options.useFactory) { 115 | return { 116 | provide: TRACING_MODULE_OPTIONS, 117 | useFactory: options.useFactory, 118 | inject: options.inject || [], 119 | }; 120 | } 121 | 122 | const inject = [ 123 | (options.useClass || options.useExisting) as Type, 124 | ]; 125 | return { 126 | provide: TRACING_MODULE_OPTIONS, 127 | useFactory: async (optionsFactory: TracingOptionsFactory) => 128 | await optionsFactory.createTracingOptions(), 129 | inject, 130 | }; 131 | } 132 | 133 | onApplicationShutdown(signal?: string): any { 134 | (TracingModule.tracer as any).close(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/util/span.util.ts: -------------------------------------------------------------------------------- 1 | import SpanContext from 'opentracing/src/span_context'; 2 | 3 | /** 4 | * Created by Rain on 2020/7/21 5 | */ 6 | export function isSpanContext(span: SpanContext | null): span is SpanContext { 7 | return span && (span as any).isValid; 8 | } 9 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/lib/util/tracing-module-options.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | import { TracingConfig, TracingOptions } from 'jaeger-client'; 5 | 6 | import { TracingModuleOptions } from '..'; 7 | 8 | export function formatTracingModuleOptions( 9 | tracingModuleOptions: TracingModuleOptions, 10 | ): { tracingConfig: TracingConfig; tracingOption: TracingOptions } { 11 | const { tracingConfig, tracingOption } = tracingModuleOptions; 12 | 13 | if (tracingOption?.contextKey) { 14 | tracingOption.contextKey = tracingOption.contextKey?.toLowerCase(); 15 | } 16 | if (tracingOption?.baggagePrefix) { 17 | tracingOption.baggagePrefix = tracingOption.baggagePrefix?.toLowerCase(); 18 | } 19 | return { tracingConfig, tracingOption }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@donews/nestjs-tracing", 3 | "version": "0.11.1", 4 | "description": "nestjs tracing module", 5 | "author": "toonewLi ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "scripts": {}, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/DoNewsCode/nestjs.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/DoNewsCode/nestjs/issues" 18 | }, 19 | "dependencies": { 20 | "axios": "1.9.0", 21 | "jaeger-client": "^3.18.1", 22 | "opentracing": "^0.14.5" 23 | }, 24 | "devDependencies": { 25 | "@grpc/grpc-js": "1.13.4", 26 | "@nestjs/axios": "4.0.0", 27 | "@nestjs/common": "11.1.3", 28 | "@nestjs/core": "11.1.3", 29 | "@nestjs/platform-express": "11.1.3", 30 | "@nestjs/testing": "11.1.3", 31 | "express": "5.1.0", 32 | "rxjs": "7.8.2" 33 | }, 34 | "peerDependencies": { 35 | "@nestjs/common": "^11.0.0", 36 | "@nestjs/core": "^11.0.0" 37 | }, 38 | "gitHead": "4e0a8578a08c92cf2229122d58940c204034ba7b" 39 | } 40 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/test/hook.module.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/22 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { Test } from '@nestjs/testing'; 7 | 8 | import { AsyncContext, AsyncHooksModule } from '../lib'; 9 | 10 | describe('hook module', () => { 11 | let asyncContext: AsyncContext; 12 | beforeEach(async () => { 13 | const asyncModule = await Test.createTestingModule({ 14 | imports: [AsyncHooksModule], 15 | }).compile(); 16 | asyncContext = asyncModule.get(AsyncContext); 17 | asyncContext.onModuleInit(); 18 | }); 19 | 20 | it('should 获得test', (done) => { 21 | asyncContext.run(() => { 22 | setTimeout(() => { 23 | asyncContext.set('test', { test: 1 }); 24 | setTimeout(() => { 25 | assert.deepStrictEqual(asyncContext.get('test'), { test: 1 }); 26 | done(); 27 | }, 200); 28 | }, 200); 29 | }); 30 | }); 31 | 32 | it('should 获得test2', (done) => { 33 | AsyncContext.getInstance().run(() => { 34 | setTimeout(() => { 35 | AsyncContext.getInstance().set('test2', { test: 1 }); 36 | setTimeout(() => { 37 | assert.deepStrictEqual(AsyncContext.getInstance().get('test2'), { 38 | test: 1, 39 | }); 40 | done(); 41 | }, 200); 42 | }, 200); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/test/test2.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/22 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { SpanD } from '../lib'; 7 | 8 | class Test2Spec { 9 | @SpanD('Test2Spc') 10 | say() { 11 | return 's'; 12 | } 13 | } 14 | 15 | describe.skip('test2', () => { 16 | it(' async context not run before spanD run ', () => { 17 | const test = new Test2Spec(); 18 | 19 | assert.strictEqual(test.say(), 's'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/test/tracing-module.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/20 3 | */ 4 | import * as assert from 'assert'; 5 | 6 | import { HttpModule, HttpService } from '@nestjs/axios'; 7 | import { INestApplication } from '@nestjs/common'; 8 | import { Test, TestingModule } from '@nestjs/testing'; 9 | import * as sinon from 'sinon'; 10 | 11 | import { TracingModule } from '../lib'; 12 | 13 | describe('http module', () => { 14 | let service: HttpService; 15 | let moduleRef: TestingModule; 16 | beforeEach(async () => { 17 | moduleRef = await Test.createTestingModule({ 18 | imports: [ 19 | TracingModule.forRoot({ 20 | tracingOption: {}, 21 | tracingConfig: { 22 | serviceName: 'foo', 23 | sampler: { param: 1, type: 'const' }, 24 | }, 25 | }), 26 | HttpModule, 27 | ], 28 | }).compile(); 29 | const app: INestApplication = moduleRef.createNestApplication(); 30 | await app.init(); 31 | service = moduleRef.get(HttpService); 32 | }); 33 | 34 | it('should start tracing span', async () => { 35 | const sandbox = sinon.createSandbox(); 36 | 37 | const tracer = TracingModule.tracer; 38 | sandbox.spy(tracer, 'startSpan'); 39 | 40 | // TODO 测试本来就无效 41 | // const result = await service.get('http://hk.jd.com', {}).toPromise(); 42 | // assert.strictEqual((tracer.startSpan as any).calledOnce, true); 43 | // sandbox.restore(); 44 | }); 45 | 46 | afterEach(() => { 47 | moduleRef.close(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/nestjs-tracing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | 5 | "declaration": true, 6 | "sourceMap": true, 7 | "removeComments": false, 8 | 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "noLib": false, 12 | "target": "ES2017", 13 | 14 | "allowJs": false, 15 | "strict": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "skipLibCheck": true, 19 | "noUnusedLocals": false 20 | }, 21 | "exclude": ["../node_modules", "./**/*.spec.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - sample/* 4 | onlyBuiltDependencies: 5 | - '@nestjs/core' 6 | - es5-ext 7 | - nx 8 | - protobufjs 9 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['config:base'], 3 | semanticCommits: 'auto', 4 | packageRules: [ 5 | { 6 | matchDepTypes: ['dependencies', 'devDependencies'], 7 | automerge: true, 8 | }, 9 | ], 10 | updateInternalDeps: true, 11 | } 12 | -------------------------------------------------------------------------------- /sample/async-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-async-hook", 3 | "private": true, 4 | "dependencies": { 5 | "@donews/nestjs-tracing": "workspace:*", 6 | "@nestjs/common": "11.1.3", 7 | "@nestjs/core": "11.1.3", 8 | "@nestjs/microservices": "11.1.3", 9 | "@nestjs/platform-express": "11.1.3", 10 | "@grpc/grpc-js": "1.13.4", 11 | "reflect-metadata": "0.2.2", 12 | "rxjs": "7.8.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample/async-hook/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { Controller, Get, Post, UseInterceptors } from '@nestjs/common'; 5 | 6 | import { TracingHttpInterceptor } from '@donews/nestjs-tracing'; 7 | 8 | import { AppService } from './app.service'; 9 | 10 | @Controller('/app') 11 | @UseInterceptors(TracingHttpInterceptor) 12 | export class AppController { 13 | constructor(private readonly appService: AppService) {} 14 | 15 | @Get() 16 | async get(): Promise { 17 | return await this.appService.get(); 18 | } 19 | 20 | @Post() 21 | async post(): Promise { 22 | return await this.appService.get(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/async-hook/src/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { Global, Module } from '@nestjs/common'; 5 | import { TracingConfig } from 'jaeger-client'; 6 | 7 | import { TracingModule, TracingModuleOptions } from '@donews/nestjs-tracing'; 8 | 9 | import { AppController } from './app.controller'; 10 | import { AppService } from './app.service'; 11 | import { GrpcController } from './grpc.controller'; 12 | import { RawGrpcProvider } from './proto/raw.service'; 13 | import { DbService } from './service/db.service'; 14 | import { GrpcService } from './service/grpc.service'; 15 | 16 | const config: TracingConfig = { 17 | serviceName: 'nest-async-hook-server', 18 | sampler: { 19 | param: 1, 20 | type: 'const', 21 | }, 22 | }; 23 | const options = { 24 | tags: { 25 | server2: '1.1.2', 26 | }, 27 | }; 28 | 29 | @Global() 30 | @Module({ 31 | imports: [ 32 | // TracingModule.forRoot({ tracingConfig: config, tracingOption: options }), 33 | TracingModule.forRootAsync({ 34 | useFactory(option: any) { 35 | return { 36 | tracingConfig: option.tracingConfig, 37 | tracingOption: option.tracingOption, 38 | } as TracingModuleOptions; 39 | }, 40 | inject: ['configProvider'], 41 | }), 42 | ], 43 | providers: [ 44 | { 45 | provide: 'configProvider', 46 | useValue: { 47 | tracingConfig: { 48 | serviceName: 'async-module-nest-async-hook-server', 49 | sampler: { 50 | param: 1, 51 | type: 'const', 52 | }, 53 | }, 54 | traceOption: { 55 | tags: { 56 | server2: '1.1.2', 57 | }, 58 | }, 59 | }, 60 | }, 61 | RawGrpcProvider, 62 | DbService, 63 | GrpcService, 64 | AppService, 65 | ], 66 | controllers: [AppController, GrpcController], 67 | exports: ['configProvider'], 68 | }) 69 | export class AppModule {} 70 | -------------------------------------------------------------------------------- /sample/async-hook/src/app.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | import { DbService } from './service/db.service'; 7 | import { GrpcService } from './service/grpc.service'; 8 | 9 | @Injectable() 10 | export class AppService { 11 | constructor( 12 | private readonly dbService: DbService, 13 | private readonly grpcService: GrpcService, 14 | ) {} 15 | 16 | async get(): Promise { 17 | const dbService = await this.dbService.find(); 18 | const grpcService = await this.grpcService.getService(); 19 | 20 | return { dbService, grpcService }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sample/async-hook/src/grpc.controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | import { join } from 'path'; 5 | 6 | import { Controller } from '@nestjs/common'; 7 | import { ClientOptions, GrpcMethod, Transport } from '@nestjs/microservices'; 8 | 9 | export const grpcClientOptions: ClientOptions = { 10 | transport: Transport.GRPC, 11 | options: { 12 | url: '0.0.0.0:4001', 13 | package: 'rawpackage', 14 | protoPath: join(__dirname, './proto/raw.proto'), 15 | }, 16 | }; 17 | 18 | @Controller() 19 | export class GrpcController { 20 | @GrpcMethod('RawService', 'Say') 21 | async say( 22 | data: { message: string }, 23 | metadata: Record, 24 | ): Promise<{ data: string }> { 25 | await sleep(Math.random() * 1000); 26 | return { data: 'tes' }; 27 | } 28 | } 29 | 30 | export function sleep(time: number): Promise { 31 | return new Promise((resolve) => { 32 | setTimeout(() => resolve(), time); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /sample/async-hook/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { NestFactory } from '@nestjs/core'; 5 | 6 | import { AppModule } from './app.module'; 7 | import { grpcClientOptions } from './grpc.controller'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule); 11 | 12 | app.connectMicroservice(grpcClientOptions); 13 | await app.startAllMicroservices(); 14 | await app.listen(3001); 15 | } 16 | 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /sample/async-hook/src/proto/raw.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package rawpackage; 4 | 5 | service RawService { 6 | rpc Say (Request) returns (Response) {} 7 | } 8 | 9 | message Request { 10 | string message = 1; 11 | } 12 | 13 | message Response { 14 | string data = 1; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /sample/async-hook/src/proto/raw.service.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { FactoryProvider } from '@nestjs/common'; 4 | import { ClientProxyFactory, Transport } from '@nestjs/microservices'; 5 | import { CallOptions, credentials, Metadata } from '@grpc/grpc-js'; 6 | import { Observable } from 'rxjs'; 7 | 8 | import { tracingGrpcInterceptor } from '@donews/nestjs-tracing/lib/aop/tracing-grpc.interceptor'; 9 | 10 | export interface RawService { 11 | say( 12 | request: { message: string }, 13 | meta?: Metadata, 14 | callOptions?: CallOptions, 15 | ): Observable<{ data: string }>; 16 | } 17 | 18 | /** 19 | * Created by Rain on 2020/7/21 20 | */ 21 | export const RawGrpcProvider: FactoryProvider = { 22 | provide: 'RAW_SERVICE', 23 | useFactory: () => { 24 | return ClientProxyFactory.create({ 25 | transport: Transport.GRPC, 26 | options: { 27 | url: '0.0.0.0:4001', 28 | credentials: credentials.createInsecure(), 29 | package: 'rawpackage', 30 | protoPath: join(__dirname, './raw.proto'), 31 | channelOptions: { 32 | interceptors: [tracingGrpcInterceptor] as any, 33 | }, 34 | }, 35 | }); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /sample/async-hook/src/service/db.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | import { SpanD } from '@donews/nestjs-tracing'; 7 | 8 | @Injectable() 9 | export class DbService { 10 | @SpanD('dbService.find') 11 | find(): Promise { 12 | return new Promise((resolve) => { 13 | const randomTime = 1000 * Math.random(); 14 | setTimeout(() => { 15 | resolve(randomTime); 16 | }, randomTime); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/async-hook/src/service/grpc.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Inject, Injectable } from '@nestjs/common'; 5 | import { ClientGrpc } from '@nestjs/microservices'; 6 | import { Metadata } from 'grpc'; 7 | 8 | import { SpanD } from '@donews/nestjs-tracing'; 9 | import { RawService } from '../proto/raw.service'; 10 | 11 | @Injectable() 12 | export class GrpcService { 13 | constructor(@Inject('RAW_SERVICE') private readonly client: ClientGrpc) {} 14 | 15 | @SpanD('GrpcService.getService') 16 | async getService(): Promise<{ data: string }> { 17 | const rawService = this.client.getService('RawService'); 18 | return await rawService.say({ message: 'tse' }, new Metadata()).toPromise(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/hooks/app.controller.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { Controller, Get, Post } from '@nestjs/common'; 5 | 6 | import { AsyncContext } from '@donews/nestjs-tracing'; 7 | 8 | import { AppService } from './app.service'; 9 | 10 | @Controller('/') 11 | export class AppController { 12 | constructor( 13 | private readonly appService: AppService, 14 | private readonly asyncContext: AsyncContext, 15 | ) {} 16 | 17 | @Get('/app') 18 | async get(): Promise { 19 | this.asyncContext.set('tracing', { controller: 'get' }); 20 | return await this.appService.get(); 21 | } 22 | 23 | @Post('/') 24 | async post(): Promise { 25 | return await this.appService.get(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sample/hooks/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 5 | 6 | import { 7 | AsyncHooksMiddleware, 8 | AsyncHooksModule, 9 | } from '@donews/nestjs-tracing/'; 10 | 11 | import { AppController } from './app.controller'; 12 | import { AppService } from './app.service'; 13 | import { DbService, GrpcService } from './service'; 14 | 15 | @Module({ 16 | imports: [AsyncHooksModule], 17 | providers: [DbService, GrpcService, AppService], 18 | controllers: [AppController], 19 | }) 20 | export class AppModule implements NestModule { 21 | configure(consumer: MiddlewareConsumer): any { 22 | consumer.apply(AsyncHooksMiddleware).forRoutes(AppController); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/hooks/app.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | import { AsyncContext } from '@donews/nestjs-tracing'; 7 | 8 | import { DbService, GrpcService } from './service'; 9 | 10 | @Injectable() 11 | export class AppService { 12 | constructor( 13 | private readonly dbService: DbService, 14 | private readonly grpcService: GrpcService, 15 | private readonly asyncContext: AsyncContext, 16 | ) {} 17 | 18 | async get(): Promise { 19 | const context = this.asyncContext.get('tracing'); 20 | context.service = 'get'; 21 | this.asyncContext.set('tracing', context); 22 | 23 | const dbService = await this.dbService.find(); 24 | const grpcService = await this.grpcService.getService(); 25 | 26 | return { dbService, grpcService, test: this.asyncContext.get('tracing') }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/hooks/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/17 3 | */ 4 | import { NestFactory } from '@nestjs/core'; 5 | 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | 11 | await app.listen(3001); 12 | } 13 | 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /sample/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hooks", 3 | "private": true, 4 | "dependencies": { 5 | "@donews/nestjs-tracing": "workspace:*", 6 | "@nestjs/common": "11.1.3", 7 | "@nestjs/core": "11.1.3", 8 | "@nestjs/microservices": "11.1.3", 9 | "@nestjs/platform-express": "11.1.3", 10 | "reflect-metadata": "0.2.2", 11 | "rxjs": "7.8.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/hooks/service/db.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | import { AsyncContext } from '@donews/nestjs-tracing'; 7 | 8 | @Injectable() 9 | export class DbService { 10 | constructor(private readonly asyncContext: AsyncContext) {} 11 | 12 | find(): Promise { 13 | return new Promise((resolve) => { 14 | const context = this.asyncContext.get('tracing'); 15 | context.dbService = 'find'; 16 | this.asyncContext.set('tracing', context); 17 | 18 | const randomTime = 1000 * Math.random(); 19 | setTimeout(() => { 20 | resolve(randomTime); 21 | }, randomTime); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/hooks/service/grpc.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * created at by Rain 2020/7/18 3 | */ 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class GrpcService { 8 | getService(): Promise { 9 | return new Promise((resolve) => { 10 | const num = 1000 * Math.random(); 11 | const str = num + ':grpcService'; 12 | setTimeout(() => { 13 | resolve(str); 14 | }, num); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/hooks/service/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Rain on 2020/7/21 3 | */ 4 | export * from './db.service'; 5 | export * from './grpc.service'; 6 | -------------------------------------------------------------------------------- /tools/gulp/config.ts: -------------------------------------------------------------------------------- 1 | import { getDirs } from './util/task-helpers'; 2 | 3 | // All paths are related to the base dir 4 | export const source = 'packages'; 5 | export const samplePath = 'sample'; 6 | 7 | export const packagePaths = getDirs(source); 8 | -------------------------------------------------------------------------------- /tools/gulp/gulpfile.ts: -------------------------------------------------------------------------------- 1 | import './tasks/copy-misc'; 2 | import './tasks/clean'; 3 | import './tasks/packages'; 4 | import './tasks/samples'; 5 | -------------------------------------------------------------------------------- /tools/gulp/tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import { task, src, series } from 'gulp'; 2 | import { source } from '../config'; 3 | import * as clean from 'gulp-clean'; 4 | import * as deleteEmpty from 'delete-empty'; 5 | 6 | /** 7 | * Cleans the build output assets from the packages folders 8 | */ 9 | function cleanOutput() { 10 | return src( 11 | [ 12 | `${source}/**/*.js`, 13 | `${source}/**/*.d.ts`, 14 | `${source}/**/*.js.map`, 15 | `${source}/**/*.d.ts.map`, 16 | ], 17 | { 18 | read: false, 19 | ignore: [`${source}/**/node_modules/**`], 20 | }, 21 | ).pipe(clean()); 22 | } 23 | 24 | /** 25 | * Cleans empty dirs 26 | */ 27 | function cleanDirs(done: () => void) { 28 | deleteEmpty.sync(`${source}/`); 29 | done(); 30 | } 31 | 32 | task('clean:output', cleanOutput); 33 | task('clean:dirs', cleanDirs); 34 | task('clean:bundle', series('clean:output', 'clean:dirs')); 35 | -------------------------------------------------------------------------------- /tools/gulp/tasks/copy-misc.ts: -------------------------------------------------------------------------------- 1 | import { task, src, dest } from 'gulp'; 2 | import { packagePaths } from '../config'; 3 | 4 | /** 5 | * Copies assets like Readme.md or LICENSE from the project base path 6 | * to all the packages. 7 | */ 8 | function copyMisc(): NodeJS.ReadWriteStream { 9 | const miscFiles = src(['LICENSE', '.npmignore']); 10 | // Since `dest()` does not take a string-array, we have to append it 11 | // ourselves 12 | return packagePaths.reduce( 13 | (stream, packagePath) => stream.pipe(dest(packagePath)), 14 | miscFiles, 15 | ); 16 | } 17 | 18 | task('copy-misc', copyMisc); 19 | -------------------------------------------------------------------------------- /tools/gulp/tasks/packages.ts: -------------------------------------------------------------------------------- 1 | import { source, packagePaths } from '../config'; 2 | import { task, watch, series, dest } from 'gulp'; 3 | import { createProject } from 'gulp-typescript'; 4 | import * as sourcemaps from 'gulp-sourcemaps'; 5 | import * as log from 'fancy-log'; 6 | 7 | // Has to be a hardcoded object due to build order 8 | const packages = { 9 | 'nestjs-config': createProject('packages/nestjs-config/tsconfig.json'), 10 | 'nestjs-logger': createProject('packages/nestjs-logger/tsconfig.json'), 11 | 'nestjs-prom': createProject('packages/nestjs-prom/tsconfig.json'), 12 | 'nestjs-tracing': createProject('packages/nestjs-tracing/tsconfig.json'), 13 | }; 14 | 15 | const modules = Object.keys(packages); 16 | 17 | const distId = process.argv.indexOf('--dist'); 18 | const dist = distId < 0 ? source : process.argv[distId + 1]; 19 | 20 | /** 21 | * Watches the packages/* folder and 22 | * builds the package on file change 23 | */ 24 | function defaultTask() { 25 | log.info('Watching files..'); 26 | modules.forEach((packageName) => { 27 | watch( 28 | [`${source}/${packageName}/**/*.ts`, `${source}/${packageName}/*.ts`], 29 | series(packageName), 30 | ); 31 | }); 32 | } 33 | 34 | /** 35 | * Builds the given package 36 | * @param packageName The name of the package 37 | */ 38 | function buildPackage(packageName: string) { 39 | return packages[packageName] 40 | .src() 41 | .pipe(packages[packageName]()) 42 | .pipe(dest(`${dist}/${packageName}`)); 43 | } 44 | 45 | /** 46 | * Builds the given package and adds sourcemaps 47 | * @param packageName The name of the package 48 | */ 49 | function buildPackageDev(packageName: string) { 50 | return packages[packageName] 51 | .src() 52 | .pipe(sourcemaps.init()) 53 | .pipe(packages[packageName]()) 54 | .pipe( 55 | sourcemaps.mapSources( 56 | (sourcePath: string) => './' + sourcePath.split('/').pop(), 57 | ), 58 | ) 59 | .pipe(sourcemaps.write('.', {})) 60 | .pipe(dest(`${dist}/${packageName}`)); 61 | } 62 | 63 | modules.forEach((packageName) => { 64 | task(packageName, () => buildPackage(packageName)); 65 | task(`${packageName}:dev`, () => buildPackageDev(packageName)); 66 | }); 67 | 68 | task('common:dev', series(modules.map((packageName) => `${packageName}:dev`))); 69 | task('build', series(modules)); 70 | task('build:dev', series('common:dev')); 71 | task('default', defaultTask); 72 | -------------------------------------------------------------------------------- /tools/gulp/tasks/samples.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process'; 2 | import * as clc from 'cli-color'; 3 | import * as log from 'fancy-log'; 4 | import { task } from 'gulp'; 5 | import { resolve } from 'path'; 6 | import { promisify } from 'util'; 7 | import { samplePath } from '../config'; 8 | import { getDirs } from '../util/task-helpers'; 9 | 10 | const exec = promisify(childProcess.exec); 11 | 12 | async function executeNpmScriptInSamples( 13 | script: string, 14 | appendScript?: string, 15 | ) { 16 | const directories = getDirs(samplePath); 17 | 18 | for await (const dir of directories) { 19 | const dirName = dir.replace(resolve(__dirname, '../../../'), ''); 20 | log.info(`Running ${clc.blue(script)} in ${clc.magenta(dirName)}`); 21 | try { 22 | const result = await exec( 23 | `${script} --prefix ${dir} ${appendScript ? '-- ' + appendScript : ''}`, 24 | ); 25 | log.info( 26 | `Finished running ${clc.blue(script)} in ${clc.magenta(dirName)}`, 27 | ); 28 | if (result.stderr) { 29 | log.error(result.stderr); 30 | } 31 | if (result.stdout) { 32 | log.error(result.stdout); 33 | } 34 | } catch (err) { 35 | log.error( 36 | `Failed running ${clc.blue(script)} in ${clc.magenta(dirName)}`, 37 | ); 38 | if (err.stderr) { 39 | log.error(err.stderr); 40 | } 41 | if (err.stdout) { 42 | log.error(err.stdout); 43 | } 44 | process.exit(1); 45 | } 46 | } 47 | } 48 | 49 | task('test:samples', async () => 50 | executeNpmScriptInSamples('npm run test', '--passWithNoTests'), 51 | ); 52 | task('test:e2e:samples', async () => 53 | executeNpmScriptInSamples('npm run test:e2e', '--passWithNoTests'), 54 | ); 55 | -------------------------------------------------------------------------------- /tools/gulp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "noUnusedParameters": false, 5 | "noUnusedLocals": false, 6 | "lib": ["es2015", "dom", "es2016.array.include"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../../dist/tools/gulp", 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "noImplicitThis": true, 13 | "noEmitOnError": true, 14 | "noImplicitAny": false, 15 | "target": "es5", 16 | "types": ["node"], 17 | "typeRoots": ["./typings", "../../node_modules/@types/"], 18 | "baseUrl": "." 19 | }, 20 | "files": ["gulpfile.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /tools/gulp/util/task-helpers.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | function isDirectory(path: string) { 5 | return statSync(path).isDirectory(); 6 | } 7 | 8 | export function getFolders(dir: string) { 9 | return readdirSync(dir).filter((file) => isDirectory(join(dir, file))); 10 | } 11 | 12 | export function getDirs(base: string) { 13 | return getFolders(base).map((path) => `${base}/${path}`); 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | 5 | "declaration": false, 6 | "sourceMap": true, 7 | "removeComments": true, 8 | 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "ES2018", 12 | "noLib": false, 13 | "lib": ["ESNext"], 14 | "outDir": "dist", 15 | 16 | "allowJs": true, 17 | "noImplicitAny": false, 18 | "noUnusedLocals": false 19 | }, 20 | "include": ["packages/**/*", "integration/**/*"], 21 | "exclude": ["node_modules", "**/*.spec.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["integration/**/*", "**/*.spec.ts"], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | --------------------------------------------------------------------------------