-
Notifications
You must be signed in to change notification settings - Fork 0
feat(apps/server/server-rest): logging & cors #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
61612a6
feat: logging with console-logger
99mini 2c12ef6
feat: github api를 너무 많이 쏘지 않도록 수정
99mini f55f2cb
style: format
99mini f3315fb
feat: force fetch 추가
99mini 5189930
chore: cors
99mini 22208a6
Merge branch 'main' into server-rest/logging
99mini 88edc74
chore: resolve broken pnpm-lock
99mini ebc0351
fix: revert cors
99mini File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,28 @@ | ||
| import { HttpModule } from '@nestjs/axios'; | ||
| import { Global, Module } from '@nestjs/common'; | ||
| import { APP_INTERCEPTOR } from '@nestjs/core'; | ||
|
|
||
| import { ServerlessProxyService } from './services'; | ||
| import { ApiModule } from './services/api.module'; | ||
|
|
||
| import { LoggingInterceptor } from './interceptors'; | ||
| import { ApiCacheService, GithubApiService, ServerlessProxyService } from './services'; | ||
|
|
||
| @Global() | ||
| @Module({ | ||
| imports: [HttpModule], | ||
| providers: [ServerlessProxyService], | ||
| exports: [ServerlessProxyService], | ||
| imports: [HttpModule, ApiModule], | ||
| providers: [ | ||
| ServerlessProxyService, | ||
| { | ||
| provide: APP_INTERCEPTOR, | ||
| useClass: LoggingInterceptor, | ||
| }, | ||
| { | ||
| provide: 'LOGGING_OPTIONS', | ||
| useValue: { logResponse: false }, | ||
| }, | ||
| ApiCacheService, | ||
| GithubApiService, | ||
| ], | ||
| exports: [ServerlessProxyService, ApiModule], | ||
| }) | ||
| export class CommonModule {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './log-route.decorator'; | ||
| export * from './log-metadata.decorator'; |
14 changes: 14 additions & 0 deletions
14
apps/server/rest/src/common/decorators/log-metadata.decorator.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { SetMetadata } from '@nestjs/common'; | ||
|
|
||
| export const LOG_METADATA = 'log_metadata'; | ||
|
|
||
| export interface LogMetadata { | ||
| module?: string; | ||
| importance?: 'high' | 'medium' | 'low'; | ||
| } | ||
|
|
||
| /** | ||
| * 로깅을 위한 메타데이터를 설정하는 데코레이터 | ||
| * @param metadata 로깅에 사용할 메타데이터 | ||
| */ | ||
| export const LogMetadata = (metadata: LogMetadata) => SetMetadata(LOG_METADATA, metadata); |
28 changes: 28 additions & 0 deletions
28
apps/server/rest/src/common/decorators/log-route.decorator.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { applyDecorators } from '@nestjs/common'; | ||
|
|
||
| import { log } from '@99mini/console-logger'; | ||
|
|
||
| /** | ||
| * 라우트 호출 시 로그를 기록하는 데코레이터 | ||
| * @param message 추가 메시지 (선택사항) | ||
| */ | ||
| export function LogRoute(message?: string) { | ||
| return applyDecorators((target: any, key: string | symbol, descriptor: PropertyDescriptor) => { | ||
| const originalMethod = descriptor.value; | ||
|
|
||
| descriptor.value = function (...args: any[]) { | ||
| const className = this.constructor.name; | ||
| const methodName = String(key); | ||
|
|
||
| if (message) { | ||
| log(`[${className}.${methodName}] ${message}`); | ||
| } else { | ||
| log(`[${className}.${methodName}]`); | ||
| } | ||
|
|
||
| return originalMethod.apply(this, args); | ||
| }; | ||
|
|
||
| return descriptor; | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './decorators'; | ||
| export * from './interceptors'; |
59 changes: 59 additions & 0 deletions
59
apps/server/rest/src/common/interceptors/advanced-logging.interceptor.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { Observable } from 'rxjs'; | ||
|
||
| import { tap } from 'rxjs/operators'; | ||
|
|
||
| import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor, Optional } from '@nestjs/common'; | ||
| import { Reflector } from '@nestjs/core'; | ||
|
|
||
| import { log } from '@99mini/console-logger'; | ||
|
|
||
| import { LOG_METADATA } from '../decorators/log-metadata.decorator'; | ||
|
|
||
| @Injectable() | ||
| export class AdvancedLoggingInterceptor implements NestInterceptor { | ||
| constructor( | ||
| private reflector: Reflector, | ||
| @Optional() @Inject('LOGGING_OPTIONS') private options?: Record<string, any>, | ||
| ) {} | ||
|
|
||
| intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
| const request = context.switchToHttp().getRequest(); | ||
| const method = request.method; | ||
| const url = request.url; | ||
| const now = Date.now(); | ||
|
|
||
| // 컨트롤러와 핸들러에서 메타데이터 가져오기 | ||
| const handler = context.getHandler(); | ||
| const controller = context.getClass(); | ||
| const metadata = this.reflector.getAllAndOverride(LOG_METADATA, [handler, controller]) || {}; | ||
|
|
||
| // 요청 본문, 쿼리 파라미터, 경로 파라미터 로깅 | ||
| const body = request.body ? JSON.stringify(request.body) : ''; | ||
| const query = request.query ? JSON.stringify(request.query) : ''; | ||
| const params = request.params ? JSON.stringify(request.params) : ''; | ||
|
|
||
| // 요청 시작 로깅 | ||
| log(`[start] ${method} ${url}`); | ||
| if (Object.keys(metadata).length > 0) { | ||
| log(`[metadata] ${JSON.stringify(metadata)}`); | ||
| } | ||
|
|
||
| if (body && body !== '{}') log(`[body] ${body}`); | ||
| if (query && query !== '{}') log(`[query] ${query}`); | ||
| if (params && params !== '{}') log(`[params] ${params}`); | ||
|
|
||
| return next.handle().pipe( | ||
| tap((data) => { | ||
| const responseTime = Date.now() - now; | ||
|
|
||
| // 응답 데이터 로깅 (선택적) | ||
| if (this.options?.logResponse) { | ||
| const responseData = data ? JSON.stringify(data).substring(0, 100) : ''; | ||
| log(`[response] ${responseData}${responseData.length >= 100 ? '...' : ''}`); | ||
| } | ||
|
|
||
| // 응답 시간 로깅 | ||
| log(`[end] ${method} ${url} +${responseTime}ms`); | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './logging.interceptor'; | ||
| export * from './advanced-logging.interceptor'; |
23 changes: 23 additions & 0 deletions
23
apps/server/rest/src/common/interceptors/logging.interceptor.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Observable } from 'rxjs'; | ||
| import { tap } from 'rxjs/operators'; | ||
|
|
||
| import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; | ||
|
|
||
| import { info } from '@99mini/console-logger'; | ||
|
|
||
| @Injectable() | ||
| export class LoggingInterceptor implements NestInterceptor { | ||
| intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
| const request = context.switchToHttp().getRequest(); | ||
| const method = request.method; | ||
| const url = request.url; | ||
| const now = Date.now(); | ||
|
|
||
| return next.handle().pipe( | ||
| tap(() => { | ||
| const responseTime = Date.now() - now; | ||
| info(`${method} ${url} +${responseTime}ms`); | ||
| }), | ||
| ); | ||
| } | ||
| } |
165 changes: 165 additions & 0 deletions
165
apps/server/rest/src/common/services/api-cache.service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import NodeCache from 'node-cache'; | ||
|
|
||
| import { Injectable } from '@nestjs/common'; | ||
|
|
||
| import { info, error as logError } from '@99mini/console-logger'; | ||
|
|
||
| export type CacheKey = `github_contributions_${string}` | `wakatime_contributions_${string}`; | ||
|
|
||
| @Injectable() | ||
| export class ApiCacheService { | ||
| private cache: NodeCache; | ||
| /** | ||
| * 기본 캐시 유효 시간 (초 단위, 1시간) | ||
| */ | ||
| private readonly DEFAULT_CACHE_TTL = 3_600; | ||
| /** | ||
| * API별 마지막 요청 시간 | ||
| */ | ||
| private lastRequestTimes: Record<string, number> = {}; | ||
| /** | ||
| * 기본 요청 간 최소 간격 (1초) | ||
| */ | ||
| private readonly DEFAULT_REQUEST_DELAY = 1_000; | ||
|
|
||
| constructor() { | ||
| this.cache = new NodeCache({ stdTTL: this.DEFAULT_CACHE_TTL, checkperiod: 600 }); | ||
| } | ||
|
|
||
| /** | ||
| * 캐시 설정 | ||
| * @param key 캐시 키 | ||
| * @param data 저장할 데이터 | ||
| * @param ttl 캐시 유효 시간 (초 단위, 기본값: 1시간) | ||
| */ | ||
| set<T>(key: CacheKey, data: T, ttl?: number): boolean { | ||
| return this.cache.set(key, data, ttl || this.DEFAULT_CACHE_TTL); | ||
| } | ||
|
|
||
| /** | ||
| * 캐시에서 데이터 가져오기 | ||
| * @param key 캐시 키 | ||
| * @returns 캐시된 데이터 또는 undefined | ||
| */ | ||
| get<T>(key: CacheKey): T | undefined { | ||
| return this.cache.get<T>(key); | ||
| } | ||
|
|
||
| /** | ||
| * 캐시 키가 존재하는지 확인 | ||
| * @param key 캐시 키 | ||
| * @returns 존재 여부 | ||
| */ | ||
| has(key: CacheKey): boolean { | ||
| return this.cache.has(key); | ||
| } | ||
|
|
||
| /** | ||
| * 캐시 유효 시간 재설정 | ||
| * @param key 캐시 키 | ||
| * @param ttl 새로운 유효 시간 (초 단위) | ||
| * @returns 성공 여부 | ||
| */ | ||
| ttl(key: CacheKey, ttl: number): boolean { | ||
| return this.cache.ttl(key, ttl); | ||
| } | ||
|
|
||
| /** | ||
| * 캐시 키 목록 가져오기 | ||
| * @returns 캐시 키 배열 | ||
| */ | ||
| keys(): CacheKey[] { | ||
| return this.cache.keys() as CacheKey[]; | ||
| } | ||
|
|
||
| /** | ||
| * 캐시에서 데이터 삭제 | ||
| * @param key 캐시 키 | ||
| * @returns 삭제 성공 여부 | ||
| */ | ||
| del(key: CacheKey): boolean { | ||
| return this.cache.del(key) > 0; | ||
| } | ||
|
|
||
| /** | ||
| * API 요청 간 간격을 조절하는 함수 | ||
| * @param apiKey API 식별자 (여러 API를 구분하기 위한 키) | ||
| * @param delay 요청 간 최소 간격 (밀리초, 기본값: 1000ms) | ||
| */ | ||
| async delayRequest(apiKey: string = 'default', delay?: number): Promise<void> { | ||
| const requestDelay = delay || this.DEFAULT_REQUEST_DELAY; | ||
| const now = Date.now(); | ||
| const lastTime = this.lastRequestTimes[apiKey] || 0; | ||
| const timeSinceLastRequest = now - lastTime; | ||
|
|
||
| if (timeSinceLastRequest < requestDelay) { | ||
| const delayTime = requestDelay - timeSinceLastRequest; | ||
| await new Promise((resolve) => setTimeout(resolve, delayTime)); | ||
| } | ||
|
|
||
| this.lastRequestTimes[apiKey] = Date.now(); | ||
| } | ||
|
|
||
| /** | ||
| * 만료된 캐시 데이터 포함하여 조회 시도 | ||
| * @param key 캐시 키 | ||
| * @returns 캐시된 데이터 또는 undefined | ||
| */ | ||
| getExpired<T>(key: CacheKey): T | undefined { | ||
| try { | ||
| // 키가 존재하는지 확인 | ||
| const keys = this.cache.keys(); | ||
| const foundKey = keys.find((k) => k === key); | ||
|
|
||
| if (foundKey) { | ||
| const data = this.cache.get<T>(key); | ||
| if (data) { | ||
| // 캐시 유효 시간 재설정 | ||
| this.cache.ttl(key, this.DEFAULT_CACHE_TTL); | ||
| return data; | ||
| } | ||
| } | ||
| return undefined; | ||
| } catch (cacheError: any) { | ||
| logError(`캐시 접근 오류: ${cacheError?.message || '알 수 없는 오류'}`); | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 캐시된 데이터 반환 또는 없을 경우 함수 실행 후 캐싱 | ||
| * @param options {key: CacheKey, ttl: 캐시 유효 시간 (초), force: 캐시 무시 } | ||
| * @param fetchFn 데이터 가져오는 비동기 함수 | ||
| * @returns 데이터 | ||
| */ | ||
| async getOrFetch<T>( | ||
| options: { key: CacheKey; ttl?: number; force?: boolean }, | ||
| fetchFn: () => Promise<T>, | ||
| ): Promise<T> { | ||
| const { key, ttl, force } = options; | ||
| const cachedData = this.get<T>(key); | ||
| if (cachedData && !force) { | ||
| info(`캐시된 데이터 반환: ${key}`); | ||
| return cachedData; | ||
| } | ||
|
|
||
| try { | ||
| // 데이터 가져오기 | ||
| const data = await fetchFn(); | ||
|
|
||
| // 결과 캐싱 | ||
| this.set(key, data, ttl); | ||
|
|
||
| return data; | ||
| } catch (error: any) { | ||
| // 에러 발생 시 만료된 캐시 데이터 확인 | ||
| const expiredData = this.getExpired<T>(key); | ||
| if (expiredData) { | ||
| info(`오류 발생으로 만료된 캐시 데이터 반환: ${key}`); | ||
| return expiredData; | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { Module } from '@nestjs/common'; | ||
|
|
||
| import { ApiCacheService } from './api-cache.service'; | ||
| import { GithubApiService } from './github-api.service'; | ||
|
|
||
| @Module({ | ||
| providers: [ApiCacheService, GithubApiService], | ||
| exports: [ApiCacheService, GithubApiService], | ||
| }) | ||
| export class ApiModule {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
LogRoutedecorator is imported in health.controller.ts but never applied to any method. Consider removing the unused decorator or applying it where needed.