mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 13:54:02 +00:00
feat(observability): add structured pino logging
This commit is contained in:
16
.env.example
16
.env.example
@@ -1,10 +1,12 @@
|
|||||||
# Core
|
# Core
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
APP_URL=http://localhost:3000
|
PORT=3000
|
||||||
|
|
||||||
# Database / Supabase
|
# Database
|
||||||
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres
|
||||||
|
|
||||||
|
# Optional Supabase
|
||||||
SUPABASE_URL=https://your-project-ref.supabase.co
|
SUPABASE_URL=https://your-project-ref.supabase.co
|
||||||
SUPABASE_PUBLISHABLE_KEY=your-supabase-publishable-key
|
SUPABASE_PUBLISHABLE_KEY=your-supabase-publishable-key
|
||||||
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
|
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
|
||||||
@@ -12,25 +14,29 @@ SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
|
|||||||
# Telegram
|
# Telegram
|
||||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||||
TELEGRAM_WEBHOOK_SECRET=your-webhook-secret
|
TELEGRAM_WEBHOOK_SECRET=your-webhook-secret
|
||||||
TELEGRAM_BOT_USERNAME=your_bot_username
|
|
||||||
TELEGRAM_WEBHOOK_PATH=/webhook/telegram
|
TELEGRAM_WEBHOOK_PATH=/webhook/telegram
|
||||||
TELEGRAM_HOUSEHOLD_CHAT_ID=-1001234567890
|
TELEGRAM_HOUSEHOLD_CHAT_ID=-1001234567890
|
||||||
TELEGRAM_PURCHASE_TOPIC_ID=777
|
TELEGRAM_PURCHASE_TOPIC_ID=777
|
||||||
|
TELEGRAM_FEEDBACK_TOPIC_ID=888
|
||||||
|
|
||||||
# Household
|
# Household
|
||||||
HOUSEHOLD_ID=11111111-1111-4111-8111-111111111111
|
HOUSEHOLD_ID=11111111-1111-4111-8111-111111111111
|
||||||
|
|
||||||
|
# Mini app
|
||||||
|
MINI_APP_ALLOWED_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
# Parsing / AI
|
# Parsing / AI
|
||||||
OPENAI_API_KEY=your-openai-api-key
|
OPENAI_API_KEY=your-openai-api-key
|
||||||
PARSER_MODEL=gpt-4.1-mini
|
PARSER_MODEL=gpt-4.1-mini
|
||||||
|
|
||||||
# Monitoring
|
# Optional monitoring
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|
||||||
# GCP
|
# Optional GCP / deploy
|
||||||
GCP_PROJECT_ID=your-gcp-project-id
|
GCP_PROJECT_ID=your-gcp-project-id
|
||||||
GCP_REGION=europe-west1
|
GCP_REGION=europe-west1
|
||||||
CLOUD_RUN_SERVICE_BOT=household-bot
|
CLOUD_RUN_SERVICE_BOT=household-bot
|
||||||
|
|
||||||
# Scheduler
|
# Scheduler
|
||||||
SCHEDULER_SHARED_SECRET=your-scheduler-shared-secret
|
SCHEDULER_SHARED_SECRET=your-scheduler-shared-secret
|
||||||
|
SCHEDULER_OIDC_ALLOWED_EMAILS=scheduler-invoker@your-project.iam.gserviceaccount.com
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@household/application": "workspace:*",
|
"@household/application": "workspace:*",
|
||||||
"@household/db": "workspace:*",
|
"@household/db": "workspace:*",
|
||||||
"@household/domain": "workspace:*",
|
"@household/domain": "workspace:*",
|
||||||
|
"@household/observability": "workspace:*",
|
||||||
"@household/ports": "workspace:*",
|
"@household/ports": "workspace:*",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"google-auth-library": "^10.4.1",
|
"google-auth-library": "^10.4.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AnonymousFeedbackService } from '@household/application'
|
import type { AnonymousFeedbackService } from '@household/application'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
import type { Bot, Context } from 'grammy'
|
import type { Bot, Context } from 'grammy'
|
||||||
|
|
||||||
function isPrivateChat(ctx: Context): boolean {
|
function isPrivateChat(ctx: Context): boolean {
|
||||||
@@ -33,6 +34,7 @@ export function registerAnonymousFeedback(options: {
|
|||||||
anonymousFeedbackService: AnonymousFeedbackService
|
anonymousFeedbackService: AnonymousFeedbackService
|
||||||
householdChatId: string
|
householdChatId: string
|
||||||
feedbackTopicId: number
|
feedbackTopicId: number
|
||||||
|
logger?: Logger
|
||||||
}): void {
|
}): void {
|
||||||
options.bot.command('anon', async (ctx) => {
|
options.bot.command('anon', async (ctx) => {
|
||||||
if (!isPrivateChat(ctx)) {
|
if (!isPrivateChat(ctx)) {
|
||||||
@@ -94,6 +96,16 @@ export function registerAnonymousFeedback(options: {
|
|||||||
await ctx.reply('Anonymous feedback delivered.')
|
await ctx.reply('Anonymous feedback delivered.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown Telegram send failure'
|
const message = error instanceof Error ? error.message : 'Unknown Telegram send failure'
|
||||||
|
options.logger?.error(
|
||||||
|
{
|
||||||
|
event: 'anonymous_feedback.post_failed',
|
||||||
|
submissionId: result.submissionId,
|
||||||
|
householdChatId: options.householdChatId,
|
||||||
|
feedbackTopicId: options.feedbackTopicId,
|
||||||
|
error: message
|
||||||
|
},
|
||||||
|
'Anonymous feedback posting failed'
|
||||||
|
)
|
||||||
await options.anonymousFeedbackService.markFailed(result.submissionId, message)
|
await options.anonymousFeedbackService.markFailed(result.submissionId, message)
|
||||||
await ctx.reply('Anonymous feedback was saved, but posting failed. Try again later.')
|
await ctx.reply('Anonymous feedback was saved, but posting failed. Try again later.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Bot } from 'grammy'
|
import { Bot } from 'grammy'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
|
|
||||||
export function createTelegramBot(token: string): Bot {
|
export function createTelegramBot(token: string, logger?: Logger): Bot {
|
||||||
const bot = new Bot(token)
|
const bot = new Bot(token)
|
||||||
|
|
||||||
bot.command('help', async (ctx) => {
|
bot.command('help', async (ctx) => {
|
||||||
@@ -20,7 +21,14 @@ export function createTelegramBot(token: string): Bot {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bot.catch((error) => {
|
bot.catch((error) => {
|
||||||
console.error('Telegram bot error', error.error)
|
logger?.error(
|
||||||
|
{
|
||||||
|
event: 'telegram.bot_error',
|
||||||
|
updateId: error.ctx?.update.update_id,
|
||||||
|
error: error.error
|
||||||
|
},
|
||||||
|
'Telegram bot error'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return bot
|
return bot
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface BotRuntimeConfig {
|
export interface BotRuntimeConfig {
|
||||||
port: number
|
port: number
|
||||||
|
logLevel: 'debug' | 'info' | 'warn' | 'error'
|
||||||
telegramBotToken: string
|
telegramBotToken: string
|
||||||
telegramWebhookSecret: string
|
telegramWebhookSecret: string
|
||||||
telegramWebhookPath: string
|
telegramWebhookPath: string
|
||||||
@@ -33,6 +34,25 @@ function parsePort(raw: string | undefined): number {
|
|||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseLogLevel(raw: string | undefined): 'debug' | 'info' | 'warn' | 'error' {
|
||||||
|
if (raw === undefined) {
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = raw.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalized === 'debug' ||
|
||||||
|
normalized === 'info' ||
|
||||||
|
normalized === 'warn' ||
|
||||||
|
normalized === 'error'
|
||||||
|
) {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid LOG_LEVEL value: ${raw}`)
|
||||||
|
}
|
||||||
|
|
||||||
function requireValue(value: string | undefined, key: string): string {
|
function requireValue(value: string | undefined, key: string): string {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
throw new Error(`${key} environment variable is required`)
|
throw new Error(`${key} environment variable is required`)
|
||||||
@@ -103,6 +123,7 @@ export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRu
|
|||||||
|
|
||||||
const runtime: BotRuntimeConfig = {
|
const runtime: BotRuntimeConfig = {
|
||||||
port: parsePort(env.PORT),
|
port: parsePort(env.PORT),
|
||||||
|
logLevel: parseLogLevel(env.LOG_LEVEL),
|
||||||
telegramBotToken: requireValue(env.TELEGRAM_BOT_TOKEN, 'TELEGRAM_BOT_TOKEN'),
|
telegramBotToken: requireValue(env.TELEGRAM_BOT_TOKEN, 'TELEGRAM_BOT_TOKEN'),
|
||||||
telegramWebhookSecret: requireValue(env.TELEGRAM_WEBHOOK_SECRET, 'TELEGRAM_WEBHOOK_SECRET'),
|
telegramWebhookSecret: requireValue(env.TELEGRAM_WEBHOOK_SECRET, 'TELEGRAM_WEBHOOK_SECRET'),
|
||||||
telegramWebhookPath: env.TELEGRAM_WEBHOOK_PATH ?? '/webhook/telegram',
|
telegramWebhookPath: env.TELEGRAM_WEBHOOK_PATH ?? '/webhook/telegram',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
createDbFinanceRepository,
|
createDbFinanceRepository,
|
||||||
createDbReminderDispatchRepository
|
createDbReminderDispatchRepository
|
||||||
} from '@household/adapters-db'
|
} from '@household/adapters-db'
|
||||||
|
import { configureLogger, getLogger } from '@household/observability'
|
||||||
|
|
||||||
import { registerAnonymousFeedback } from './anonymous-feedback'
|
import { registerAnonymousFeedback } from './anonymous-feedback'
|
||||||
import { createFinanceCommandsService } from './finance-commands'
|
import { createFinanceCommandsService } from './finance-commands'
|
||||||
@@ -27,7 +28,13 @@ import { createMiniAppAuthHandler } from './miniapp-auth'
|
|||||||
import { createMiniAppDashboardHandler } from './miniapp-dashboard'
|
import { createMiniAppDashboardHandler } from './miniapp-dashboard'
|
||||||
|
|
||||||
const runtime = getBotRuntimeConfig()
|
const runtime = getBotRuntimeConfig()
|
||||||
const bot = createTelegramBot(runtime.telegramBotToken)
|
configureLogger({
|
||||||
|
level: runtime.logLevel,
|
||||||
|
service: '@household/bot'
|
||||||
|
})
|
||||||
|
|
||||||
|
const logger = getLogger('runtime')
|
||||||
|
const bot = createTelegramBot(runtime.telegramBotToken, getLogger('telegram'))
|
||||||
const webhookHandler = webhookCallback(bot, 'std/http')
|
const webhookHandler = webhookCallback(bot, 'std/http')
|
||||||
|
|
||||||
const shutdownTasks: Array<() => Promise<void>> = []
|
const shutdownTasks: Array<() => Promise<void>> = []
|
||||||
@@ -66,14 +73,21 @@ if (runtime.purchaseTopicIngestionEnabled) {
|
|||||||
purchaseTopicId: runtime.telegramPurchaseTopicId!
|
purchaseTopicId: runtime.telegramPurchaseTopicId!
|
||||||
},
|
},
|
||||||
purchaseRepositoryClient.repository,
|
purchaseRepositoryClient.repository,
|
||||||
llmFallback
|
{
|
||||||
? {
|
...(llmFallback
|
||||||
llmFallback
|
? {
|
||||||
}
|
llmFallback
|
||||||
: {}
|
}
|
||||||
|
: {}),
|
||||||
|
logger: getLogger('purchase-ingestion')
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
logger.warn(
|
||||||
|
{
|
||||||
|
event: 'runtime.feature_disabled',
|
||||||
|
feature: 'purchase-topic-ingestion'
|
||||||
|
},
|
||||||
'Purchase topic ingestion is disabled. Set DATABASE_URL, HOUSEHOLD_ID, TELEGRAM_HOUSEHOLD_CHAT_ID, and TELEGRAM_PURCHASE_TOPIC_ID to enable.'
|
'Purchase topic ingestion is disabled. Set DATABASE_URL, HOUSEHOLD_ID, TELEGRAM_HOUSEHOLD_CHAT_ID, and TELEGRAM_PURCHASE_TOPIC_ID to enable.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -83,7 +97,13 @@ if (runtime.financeCommandsEnabled) {
|
|||||||
|
|
||||||
financeCommands.register(bot)
|
financeCommands.register(bot)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Finance commands are disabled. Set DATABASE_URL and HOUSEHOLD_ID to enable.')
|
logger.warn(
|
||||||
|
{
|
||||||
|
event: 'runtime.feature_disabled',
|
||||||
|
feature: 'finance-commands'
|
||||||
|
},
|
||||||
|
'Finance commands are disabled. Set DATABASE_URL and HOUSEHOLD_ID to enable.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reminderJobs = runtime.reminderJobsEnabled
|
const reminderJobs = runtime.reminderJobsEnabled
|
||||||
@@ -95,13 +115,18 @@ const reminderJobs = runtime.reminderJobsEnabled
|
|||||||
|
|
||||||
return createReminderJobsHandler({
|
return createReminderJobsHandler({
|
||||||
householdId: runtime.householdId!,
|
householdId: runtime.householdId!,
|
||||||
reminderService
|
reminderService,
|
||||||
|
logger: getLogger('scheduler')
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (!runtime.reminderJobsEnabled) {
|
if (!runtime.reminderJobsEnabled) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
|
{
|
||||||
|
event: 'runtime.feature_disabled',
|
||||||
|
feature: 'reminder-jobs'
|
||||||
|
},
|
||||||
'Reminder jobs are disabled. Set DATABASE_URL, HOUSEHOLD_ID, and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
|
'Reminder jobs are disabled. Set DATABASE_URL, HOUSEHOLD_ID, and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -111,10 +136,15 @@ if (anonymousFeedbackService) {
|
|||||||
bot,
|
bot,
|
||||||
anonymousFeedbackService,
|
anonymousFeedbackService,
|
||||||
householdChatId: runtime.telegramHouseholdChatId!,
|
householdChatId: runtime.telegramHouseholdChatId!,
|
||||||
feedbackTopicId: runtime.telegramFeedbackTopicId!
|
feedbackTopicId: runtime.telegramFeedbackTopicId!,
|
||||||
|
logger: getLogger('anonymous-feedback')
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
logger.warn(
|
||||||
|
{
|
||||||
|
event: 'runtime.feature_disabled',
|
||||||
|
feature: 'anonymous-feedback'
|
||||||
|
},
|
||||||
'Anonymous feedback is disabled. Set DATABASE_URL, HOUSEHOLD_ID, TELEGRAM_HOUSEHOLD_CHAT_ID, and TELEGRAM_FEEDBACK_TOPIC_ID to enable.'
|
'Anonymous feedback is disabled. Set DATABASE_URL, HOUSEHOLD_ID, TELEGRAM_HOUSEHOLD_CHAT_ID, and TELEGRAM_FEEDBACK_TOPIC_ID to enable.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,14 +157,16 @@ const server = createBotWebhookServer({
|
|||||||
? createMiniAppAuthHandler({
|
? createMiniAppAuthHandler({
|
||||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||||
botToken: runtime.telegramBotToken,
|
botToken: runtime.telegramBotToken,
|
||||||
repository: financeRepositoryClient.repository
|
repository: financeRepositoryClient.repository,
|
||||||
|
logger: getLogger('miniapp-auth')
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
miniAppDashboard: financeService
|
miniAppDashboard: financeService
|
||||||
? createMiniAppDashboardHandler({
|
? createMiniAppDashboardHandler({
|
||||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||||
botToken: runtime.telegramBotToken,
|
botToken: runtime.telegramBotToken,
|
||||||
financeService
|
financeService,
|
||||||
|
logger: getLogger('miniapp-dashboard')
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
scheduler:
|
scheduler:
|
||||||
@@ -162,11 +194,24 @@ if (import.meta.main) {
|
|||||||
fetch: server.fetch
|
fetch: server.fetch
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
logger.info(
|
||||||
`@household/bot webhook server started on :${runtime.port} path=${runtime.telegramWebhookPath}`
|
{
|
||||||
|
event: 'runtime.started',
|
||||||
|
port: runtime.port,
|
||||||
|
webhookPath: runtime.telegramWebhookPath
|
||||||
|
},
|
||||||
|
'Bot webhook server started'
|
||||||
)
|
)
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
event: 'runtime.shutdown',
|
||||||
|
signal: 'SIGTERM'
|
||||||
|
},
|
||||||
|
'Bot shutdown requested'
|
||||||
|
)
|
||||||
|
|
||||||
for (const close of shutdownTasks) {
|
for (const close of shutdownTasks) {
|
||||||
void close()
|
void close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FinanceMemberRecord, FinanceRepository } from '@household/ports'
|
import type { FinanceMemberRecord, FinanceRepository } from '@household/ports'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
|
|
||||||
import { verifyTelegramMiniAppInitData } from './telegram-miniapp-auth'
|
import { verifyTelegramMiniAppInitData } from './telegram-miniapp-auth'
|
||||||
|
|
||||||
@@ -60,18 +61,19 @@ export async function readMiniAppInitData(request: Request): Promise<string | nu
|
|||||||
return initData && initData.length > 0 ? initData : null
|
return initData && initData.length > 0 ? initData : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function miniAppErrorResponse(error: unknown, origin?: string): Response {
|
export function miniAppErrorResponse(error: unknown, origin?: string, logger?: Logger): Response {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown mini app error'
|
const message = error instanceof Error ? error.message : 'Unknown mini app error'
|
||||||
|
|
||||||
if (message === 'Invalid JSON body') {
|
if (message === 'Invalid JSON body') {
|
||||||
return miniAppJsonResponse({ ok: false, error: message }, 400, origin)
|
return miniAppJsonResponse({ ok: false, error: message }, 400, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(
|
logger?.error(
|
||||||
JSON.stringify({
|
{
|
||||||
event: 'miniapp.request_failed',
|
event: 'miniapp.request_failed',
|
||||||
error: message
|
error: message
|
||||||
})
|
},
|
||||||
|
'Mini app request failed'
|
||||||
)
|
)
|
||||||
|
|
||||||
return miniAppJsonResponse({ ok: false, error: 'Internal Server Error' }, 500, origin)
|
return miniAppJsonResponse({ ok: false, error: 'Internal Server Error' }, 500, origin)
|
||||||
@@ -128,6 +130,7 @@ export function createMiniAppAuthHandler(options: {
|
|||||||
allowedOrigins: readonly string[]
|
allowedOrigins: readonly string[]
|
||||||
botToken: string
|
botToken: string
|
||||||
repository: FinanceRepository
|
repository: FinanceRepository
|
||||||
|
logger?: Logger
|
||||||
}): {
|
}): {
|
||||||
handler: (request: Request) => Promise<Response>
|
handler: (request: Request) => Promise<Response>
|
||||||
} {
|
} {
|
||||||
@@ -190,7 +193,7 @@ export function createMiniAppAuthHandler(options: {
|
|||||||
origin
|
origin
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return miniAppErrorResponse(error, origin)
|
return miniAppErrorResponse(error, origin, options.logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FinanceCommandService } from '@household/application'
|
import type { FinanceCommandService } from '@household/application'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
allowedMiniAppOrigin,
|
allowedMiniAppOrigin,
|
||||||
@@ -12,6 +13,7 @@ export function createMiniAppDashboardHandler(options: {
|
|||||||
allowedOrigins: readonly string[]
|
allowedOrigins: readonly string[]
|
||||||
botToken: string
|
botToken: string
|
||||||
financeService: FinanceCommandService
|
financeService: FinanceCommandService
|
||||||
|
logger?: Logger
|
||||||
}): {
|
}): {
|
||||||
handler: (request: Request) => Promise<Response>
|
handler: (request: Request) => Promise<Response>
|
||||||
} {
|
} {
|
||||||
@@ -99,7 +101,7 @@ export function createMiniAppDashboardHandler(options: {
|
|||||||
origin
|
origin
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return miniAppErrorResponse(error, origin)
|
return miniAppErrorResponse(error, origin, options.logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parsePurchaseMessage, type PurchaseParserLlmFallback } from '@household/application'
|
import { parsePurchaseMessage, type PurchaseParserLlmFallback } from '@household/application'
|
||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import type { Bot, Context } from 'grammy'
|
import type { Bot, Context } from 'grammy'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
|
|
||||||
import { createDbClient, schema } from '@household/db'
|
import { createDbClient, schema } from '@household/db'
|
||||||
|
|
||||||
@@ -197,6 +198,7 @@ export function registerPurchaseTopicIngestion(
|
|||||||
repository: PurchaseMessageIngestionRepository,
|
repository: PurchaseMessageIngestionRepository,
|
||||||
options: {
|
options: {
|
||||||
llmFallback?: PurchaseParserLlmFallback
|
llmFallback?: PurchaseParserLlmFallback
|
||||||
|
logger?: Logger
|
||||||
} = {}
|
} = {}
|
||||||
): void {
|
): void {
|
||||||
bot.on('message:text', async (ctx, next) => {
|
bot.on('message:text', async (ctx, next) => {
|
||||||
@@ -216,12 +218,30 @@ export function registerPurchaseTopicIngestion(
|
|||||||
const status = await repository.save(record, options.llmFallback)
|
const status = await repository.save(record, options.llmFallback)
|
||||||
|
|
||||||
if (status === 'created') {
|
if (status === 'created') {
|
||||||
console.log(
|
options.logger?.info(
|
||||||
`purchase topic message ingested chat=${record.chatId} thread=${record.threadId} message=${record.messageId}`
|
{
|
||||||
|
event: 'purchase.ingested',
|
||||||
|
chatId: record.chatId,
|
||||||
|
threadId: record.threadId,
|
||||||
|
messageId: record.messageId,
|
||||||
|
updateId: record.updateId,
|
||||||
|
senderTelegramUserId: record.senderTelegramUserId
|
||||||
|
},
|
||||||
|
'Purchase topic message ingested'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to ingest purchase topic message', error)
|
options.logger?.error(
|
||||||
|
{
|
||||||
|
event: 'purchase.ingest_failed',
|
||||||
|
chatId: record.chatId,
|
||||||
|
threadId: record.threadId,
|
||||||
|
messageId: record.messageId,
|
||||||
|
updateId: record.updateId,
|
||||||
|
error
|
||||||
|
},
|
||||||
|
'Failed to ingest purchase topic message'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReminderJobService } from '@household/application'
|
import type { ReminderJobService } from '@household/application'
|
||||||
import { BillingPeriod } from '@household/domain'
|
import { BillingPeriod } from '@household/domain'
|
||||||
|
import type { Logger } from '@household/observability'
|
||||||
import { REMINDER_TYPES, type ReminderType } from '@household/ports'
|
import { REMINDER_TYPES, type ReminderType } from '@household/ports'
|
||||||
|
|
||||||
interface ReminderJobRequestBody {
|
interface ReminderJobRequestBody {
|
||||||
@@ -51,6 +52,7 @@ export function createReminderJobsHandler(options: {
|
|||||||
householdId: string
|
householdId: string
|
||||||
reminderService: ReminderJobService
|
reminderService: ReminderJobService
|
||||||
forceDryRun?: boolean
|
forceDryRun?: boolean
|
||||||
|
logger?: Logger
|
||||||
}): {
|
}): {
|
||||||
handle: (request: Request, rawReminderType: string) => Promise<Response>
|
handle: (request: Request, rawReminderType: string) => Promise<Response>
|
||||||
} {
|
} {
|
||||||
@@ -83,7 +85,7 @@ export function createReminderJobsHandler(options: {
|
|||||||
dryRun
|
dryRun
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(JSON.stringify(logPayload))
|
options.logger?.info(logPayload, 'Reminder job processed')
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -98,12 +100,13 @@ export function createReminderJobsHandler(options: {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown reminder job error'
|
const message = error instanceof Error ? error.message : 'Unknown reminder job error'
|
||||||
|
|
||||||
console.error(
|
options.logger?.error(
|
||||||
JSON.stringify({
|
{
|
||||||
event: 'scheduler.reminder.dispatch_failed',
|
event: 'scheduler.reminder.dispatch_failed',
|
||||||
reminderType: rawReminderType,
|
reminderType: rawReminderType,
|
||||||
error: message
|
error: message
|
||||||
})
|
},
|
||||||
|
'Reminder job failed'
|
||||||
)
|
)
|
||||||
|
|
||||||
return json({ ok: false, error: message }, 400)
|
return json({ ok: false, error: message }, 400)
|
||||||
|
|||||||
30
bun.lock
30
bun.lock
@@ -20,6 +20,7 @@
|
|||||||
"@household/application": "workspace:*",
|
"@household/application": "workspace:*",
|
||||||
"@household/db": "workspace:*",
|
"@household/db": "workspace:*",
|
||||||
"@household/domain": "workspace:*",
|
"@household/domain": "workspace:*",
|
||||||
|
"@household/observability": "workspace:*",
|
||||||
"@household/ports": "workspace:*",
|
"@household/ports": "workspace:*",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"google-auth-library": "^10.4.1",
|
"google-auth-library": "^10.4.1",
|
||||||
@@ -78,6 +79,9 @@
|
|||||||
},
|
},
|
||||||
"packages/observability": {
|
"packages/observability": {
|
||||||
"name": "@household/observability",
|
"name": "@household/observability",
|
||||||
|
"dependencies": {
|
||||||
|
"pino": "^9.9.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"packages/ports": {
|
"packages/ports": {
|
||||||
"name": "@household/ports",
|
"name": "@household/ports",
|
||||||
@@ -267,6 +271,8 @@
|
|||||||
|
|
||||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.51.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw=="],
|
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.51.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw=="],
|
||||||
|
|
||||||
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
@@ -415,6 +421,8 @@
|
|||||||
|
|
||||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||||
|
|
||||||
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="],
|
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.5", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg=="],
|
||||||
|
|
||||||
"babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
"babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||||
@@ -601,6 +609,8 @@
|
|||||||
|
|
||||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||||
|
|
||||||
|
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.51.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.51.0", "@oxlint/binding-android-arm64": "1.51.0", "@oxlint/binding-darwin-arm64": "1.51.0", "@oxlint/binding-darwin-x64": "1.51.0", "@oxlint/binding-freebsd-x64": "1.51.0", "@oxlint/binding-linux-arm-gnueabihf": "1.51.0", "@oxlint/binding-linux-arm-musleabihf": "1.51.0", "@oxlint/binding-linux-arm64-gnu": "1.51.0", "@oxlint/binding-linux-arm64-musl": "1.51.0", "@oxlint/binding-linux-ppc64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-musl": "1.51.0", "@oxlint/binding-linux-s390x-gnu": "1.51.0", "@oxlint/binding-linux-x64-gnu": "1.51.0", "@oxlint/binding-linux-x64-musl": "1.51.0", "@oxlint/binding-openharmony-arm64": "1.51.0", "@oxlint/binding-win32-arm64-msvc": "1.51.0", "@oxlint/binding-win32-ia32-msvc": "1.51.0", "@oxlint/binding-win32-x64-msvc": "1.51.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ=="],
|
"oxlint": ["oxlint@1.51.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.51.0", "@oxlint/binding-android-arm64": "1.51.0", "@oxlint/binding-darwin-arm64": "1.51.0", "@oxlint/binding-darwin-x64": "1.51.0", "@oxlint/binding-freebsd-x64": "1.51.0", "@oxlint/binding-linux-arm-gnueabihf": "1.51.0", "@oxlint/binding-linux-arm-musleabihf": "1.51.0", "@oxlint/binding-linux-arm64-gnu": "1.51.0", "@oxlint/binding-linux-arm64-musl": "1.51.0", "@oxlint/binding-linux-ppc64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-musl": "1.51.0", "@oxlint/binding-linux-s390x-gnu": "1.51.0", "@oxlint/binding-linux-x64-gnu": "1.51.0", "@oxlint/binding-linux-x64-musl": "1.51.0", "@oxlint/binding-openharmony-arm64": "1.51.0", "@oxlint/binding-win32-arm64-msvc": "1.51.0", "@oxlint/binding-win32-ia32-msvc": "1.51.0", "@oxlint/binding-win32-x64-msvc": "1.51.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ=="],
|
||||||
|
|
||||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||||
@@ -615,10 +625,22 @@
|
|||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
||||||
|
|
||||||
|
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
||||||
|
|
||||||
|
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||||
|
|
||||||
|
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||||
|
|
||||||
|
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||||
|
|
||||||
|
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||||
@@ -627,6 +649,8 @@
|
|||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
||||||
@@ -645,12 +669,16 @@
|
|||||||
|
|
||||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||||
|
|
||||||
|
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -663,6 +691,8 @@
|
|||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|||||||
@@ -1,24 +1,49 @@
|
|||||||
import { createEnv } from '@t3-oss/env-core'
|
import { createEnv } from '@t3-oss/env-core'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
function parseOptionalCsv(value: string | undefined): readonly string[] | undefined {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed
|
||||||
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
const server = {
|
const server = {
|
||||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||||
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
||||||
APP_URL: z.string().url(),
|
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
|
||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.string().url(),
|
||||||
SUPABASE_URL: z.string().url(),
|
HOUSEHOLD_ID: z.string().uuid(),
|
||||||
SUPABASE_PUBLISHABLE_KEY: z.string().min(1),
|
SUPABASE_URL: z.string().url().optional(),
|
||||||
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
|
SUPABASE_PUBLISHABLE_KEY: z.string().min(1).optional(),
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1).optional(),
|
||||||
TELEGRAM_BOT_TOKEN: z.string().min(1),
|
TELEGRAM_BOT_TOKEN: z.string().min(1),
|
||||||
TELEGRAM_WEBHOOK_SECRET: z.string().min(1),
|
TELEGRAM_WEBHOOK_SECRET: z.string().min(1),
|
||||||
TELEGRAM_BOT_USERNAME: z.string().min(1),
|
TELEGRAM_WEBHOOK_PATH: z.string().min(1).default('/webhook/telegram'),
|
||||||
OPENAI_API_KEY: z.string().min(1),
|
TELEGRAM_HOUSEHOLD_CHAT_ID: z.string().min(1).optional(),
|
||||||
|
TELEGRAM_PURCHASE_TOPIC_ID: z.coerce.number().int().positive().optional(),
|
||||||
|
TELEGRAM_FEEDBACK_TOPIC_ID: z.coerce.number().int().positive().optional(),
|
||||||
|
MINI_APP_ALLOWED_ORIGINS: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => parseOptionalCsv(value)),
|
||||||
|
SCHEDULER_OIDC_ALLOWED_EMAILS: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => parseOptionalCsv(value)),
|
||||||
|
OPENAI_API_KEY: z.string().min(1).optional(),
|
||||||
PARSER_MODEL: z.string().min(1).default('gpt-4.1-mini'),
|
PARSER_MODEL: z.string().min(1).default('gpt-4.1-mini'),
|
||||||
SENTRY_DSN: z.string().url().optional(),
|
SENTRY_DSN: z.string().url().optional(),
|
||||||
GCP_PROJECT_ID: z.string().min(1),
|
GCP_PROJECT_ID: z.string().min(1).optional(),
|
||||||
GCP_REGION: z.string().min(1).default('europe-west1'),
|
GCP_REGION: z.string().min(1).default('europe-west1'),
|
||||||
CLOUD_RUN_SERVICE_BOT: z.string().min(1).default('household-bot'),
|
CLOUD_RUN_SERVICE_BOT: z.string().min(1).default('household-bot'),
|
||||||
SCHEDULER_SHARED_SECRET: z.string().min(1)
|
SCHEDULER_SHARED_SECRET: z.string().min(1).optional()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
"name": "@household/observability",
|
"name": "@household/observability",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||||
"typecheck": "tsgo --project tsconfig.json --noEmit",
|
"typecheck": "tsgo --project tsconfig.json --noEmit",
|
||||||
"test": "bun test --pass-with-no-tests",
|
"test": "bun test --pass-with-no-tests",
|
||||||
"lint": "oxlint \"src\""
|
"lint": "oxlint \"src\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pino": "^9.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,53 @@
|
|||||||
export const observabilityReady = true
|
import pino, { type Bindings, type Logger, type LoggerOptions } from 'pino'
|
||||||
|
|
||||||
|
export type { Logger }
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
|
||||||
|
let rootLogger = pino({
|
||||||
|
level: 'info',
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
base: null,
|
||||||
|
formatters: {
|
||||||
|
level(label) {
|
||||||
|
return {
|
||||||
|
level: label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function configureLogger(
|
||||||
|
options: {
|
||||||
|
level?: LogLevel
|
||||||
|
service?: string
|
||||||
|
base?: Bindings
|
||||||
|
} = {}
|
||||||
|
): Logger {
|
||||||
|
const loggerOptions: LoggerOptions = {
|
||||||
|
level: options.level ?? 'info',
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
base: null,
|
||||||
|
formatters: {
|
||||||
|
level(label) {
|
||||||
|
return {
|
||||||
|
level: label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootLogger = pino(loggerOptions).child({
|
||||||
|
service: options.service ?? 'household',
|
||||||
|
...options.base
|
||||||
|
})
|
||||||
|
|
||||||
|
return rootLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogger(name: string, bindings: Bindings = {}): Logger {
|
||||||
|
return rootLogger.child({
|
||||||
|
logger: name,
|
||||||
|
...bindings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user