Compare commits

...

2 Commits

Author SHA1 Message Date
4a0cad44be feat: add logs 2026-03-28 09:44:38 +03:00
a2a45d90ba feat: add opentelemetry 2026-03-27 06:46:29 +03:00
8 changed files with 1148 additions and 8 deletions

View File

@ -11,6 +11,8 @@ services:
- .env.production.local
expose:
- '9101'
volumes:
- ../logs/auth:/var/log/services/auth
networks:
- teacinema

View File

@ -30,6 +30,11 @@
"@nestjs/mapped-types": "*",
"@nestjs/microservices": "^11.1.12",
"@nestjs/platform-express": "^11.0.1",
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-node": "^0.214.0",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@teacinema/common": "^1.0.0",
@ -42,6 +47,9 @@
"class-validator": "^0.14.3",
"dotenv-expand": "^12.0.3",
"ioredis": "^5.9.2",
"nestjs-pino": "^4.6.1",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"prisma": "^7.3.0",
"prom-client": "^15.1.3",
"reflect-metadata": "^0.2.2",

View File

@ -1,5 +1,7 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { LoggerModule } from 'nestjs-pino'
import pino from 'pino'
import { ObservabilityModule } from '@/observability/observability.module'
@ -21,6 +23,23 @@ import { TelegramModule } from './modules/telegram/telegram.module'
import { TokenModule } from './modules/token/token.module'
import { UsersModule } from './modules/users/users.module'
const transport = pino.transport({
target: 'pino/file',
options: {
destination: '/var/log/services/auth/auth.log',
mkdir: true
}
})
const logger = pino(
{
level: process.env.LOG_LEVEL || 'info',
messageKey: 'msg',
base: { service: 'auth' }
},
transport
)
@Module({
imports: [
ConfigModule.forRoot({
@ -32,6 +51,9 @@ import { UsersModule } from './modules/users/users.module'
],
load: [dabataseEnv, grpcEnv, redisEnv, passportEnv, telegramEnv, rmqEnv]
}),
LoggerModule.forRoot({
pinoHttp: { logger }
}),
PrismaModule,
RedisModule,
ObservabilityModule,

View File

@ -4,6 +4,7 @@ import { NestFactory } from '@nestjs/core'
import { createGrpcServer } from '@/infra/grpc/grpc.server'
import { AppModule } from './app.module'
import './observability/tracing'
async function bootstrap() {
const app = await NestFactory.create(AppModule)

View File

@ -11,6 +11,7 @@ import {
VerifyOtpRequest,
VerifyOtpResponse
} from '@teacinema/contracts/gen/auth'
import { PinoLogger } from 'nestjs-pino'
import { MessagingService } from '@/infra/messaging/messaging.service'
import { OtpService } from '@/modules/otp/otp.service'
@ -22,17 +23,22 @@ import { UsersClientGrpc } from '../users/users.grpc'
@Injectable()
export class AuthService {
public constructor(
private readonly logger: PinoLogger,
private readonly userRepository: UserRepository,
private readonly otpService: OtpService,
private readonly tokenService: TokenService,
private readonly messagingService: MessagingService,
private readonly usersClient: UsersClientGrpc,
private readonly configService: ConfigService
) {}
) {
this.logger.setContext(AuthService.name)
}
public async sendOtp(data: SendOtpRequest): Promise<SendOtpResponse> {
const { identifier, type } = data
this.logger.debug(`OTP request received from ${identifier} of type ${type}`)
let account: Account | null = null
const isPhoneType = type === 'phone'
const isEmailType = type === 'email'
@ -44,6 +50,9 @@ export class AuthService {
}
if (!account) {
this.logger.warn(
`Account not found, creating new account for ${identifier} `
)
account = await this.userRepository.createAccount({
email: isEmailType ? identifier : undefined,
phone: isPhoneType ? identifier : undefined
@ -60,12 +69,16 @@ export class AuthService {
type: type as 'email' | 'phone'
})
this.logger.info(`OTP successfully send to ${identifier} of type ${type}`)
return { ok: true }
}
public async verifyOtp(data: VerifyOtpRequest): Promise<VerifyOtpResponse> {
const { identifier, code, type } = data
this.logger.debug(`OTP verify for ${identifier} of type ${type}`)
await this.otpService.verify(identifier, code, type as 'email' | 'phone')
const isPhoneType = type === 'phone'
@ -76,6 +89,9 @@ export class AuthService {
: await this.userRepository.findByEmail(identifier)
if (!account) {
this.logger.warn(
`Account not found, create new account for ${identifier}`
)
throw new RpcException({
code: RpcStatus.NOT_FOUND,
details: 'Account not found'
@ -94,6 +110,10 @@ export class AuthService {
})
}
this.logger.info(
`OTP verified successfully for ${identifier} of type ${type}`
)
this.usersClient.create({ id: account.id }).subscribe()
return this.tokenService.generateTokens(account.id)
@ -101,9 +121,15 @@ export class AuthService {
public refresh(data: RefreshRequest): RefreshResponse {
const { refreshToken } = data
this.logger.debug(`Refresh token request received`)
const result = this.tokenService.verify(refreshToken)
if (!result.valid) {
this.logger.error(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ok
// @ts-expect-error
`Invalid refresh token reason: ${result.reason || 'Unknown'}`
)
throw new RpcException({
code: RpcStatus.UNAUTHENTICATED,
// @ts-expect-error -- TODO: fixme
@ -111,6 +137,10 @@ export class AuthService {
})
}
this.logger.info(
`Refresh token verified successfully for user ${result.userId}`
)
return this.tokenService.generateTokens(result.userId)
}
}

View File

@ -1,17 +1,27 @@
import { Injectable } from '@nestjs/common'
import { RpcException } from '@nestjs/microservices'
import { RpcStatus } from '@teacinema/common'
import { PinoLogger } from 'nestjs-pino'
import { createHash } from 'node:crypto'
import { RedisService } from '@/infra/redis/redis.service'
@Injectable()
export class OtpService {
public constructor(private readonly redisService: RedisService) {}
public constructor(
private readonly logger: PinoLogger,
private readonly redisService: RedisService
) {
this.logger.setContext(OtpService.name)
}
public async send(indentifier: string, type: 'email' | 'phone') {
const { code, hash } = this.generateCode()
this.logger.debug(
`OTP send to ${indentifier} of type ${type}, code ${code}, hash ${hash}`
)
await this.redisService.set(
`otp:${type}:${indentifier}`,
hash,
@ -19,6 +29,8 @@ export class OtpService {
60 * 5
)
this.logger.debug(`OTP stored to Redis: ${indentifier}`)
return { code, hash }
}
@ -54,6 +66,8 @@ export class OtpService {
const code = Math.floor(100_000 + Math.random() * 900_000)
const hash = createHash('sha256').update(String(code)).digest('hex')
this.logger.debug(`Generated OTP code: ${code}`)
return { code, hash }
}
}

View File

@ -0,0 +1,27 @@
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
const traceExporter = new OTLPTraceExporter({
url: 'http://jaeger:4317'
})
const otelSdk = new NodeSDK({
traceExporter,
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'auth-service'
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-grpc': { enabled: true },
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-nestjs-core': { enabled: true },
'@opentelemetry/instrumentation-redis': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true }
})
]
})
otelSdk.start()

1048
yarn.lock

File diff suppressed because it is too large Load Diff