All files / src/libs/logger logger.ts

50.5% Statements 50/99
71.42% Branches 10/14
61.53% Functions 8/13
50.5% Lines 50/99

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158  1x           1x 1x 1x     1x 1x 1x   1x 1x           1x 1x     1x 1x 1x 1x 1x 1x 1x 1x                 1x 1x 1x 1x 1x                                                                                                                               1x 1x   1x                         3x   3x 3x 3x   16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x   1x     1x 7x 7x   1x 6x 6x   1x  
// src/libs/logger.ts
import pino, {type LoggerOptions} from 'pino'
 
type LevelName = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
type Schema = 'freelance' | 'agent' | 'corporate'
 
/* ===== 環境変数 ===== */
const SCHEMA_ENV = (import.meta.env.VITE_LOG_SCHEMA as string) || 'freelance'
export const SCHEMA: Schema = (['freelance', 'agent', 'corporate'].includes(SCHEMA_ENV)
  ? (SCHEMA_ENV as Schema)
  : 'freelance')
 
const levelFromEnv =
  (import.meta.env.VITE_LOG_LOCAL_LEVEL as string) ??
  (import.meta.env.DEV ? 'debug' : 'info')
 
const remoteUrl = import.meta.env.VITE_LOG_REMOTE_URL as string | undefined
const remoteLevel = (import.meta.env.VITE_LOG_REMOTE_LEVEL as LevelName) || 'warn'
 
/*
 * ===== pino レベル定義(マジックナンバー排除) =====
 * { trace:10, debug:20, info:30, warn:40, error:50, fatal:60 }
 */
const LV = pino.levels.values
const LABEL = pino.levels.labels as Record<number, LevelName>
 
type ConsoleMethod = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'log'
const CONSOLE_FOR: Record<LevelName, ConsoleMethod> = {
  fatal: 'error',
  error: 'error',
  warn:  'warn',
  info:  'info',
  debug: 'debug',
  trace: 'trace',
}
 
/* ===== JST (+09:00) タイムスタンプ ===== */
function isoJst(d = new Date()): string {
  const jst = new Date(d.getTime() + 9 * 60 * 60 * 1000)
  return jst.toISOString().replace('Z', '+09:00')
}
 
/* ===== pino 本体(browser) ===== */
const opts: LoggerOptions = {
  level: levelFromEnv,
  browser: {
    asObject: true,
    write(o: any) {
      // 1) console 出力(JSON 1行)
      // const line = JSON.stringify(o)
      // const method = CONSOLE_FOR[LABEL[o.level]] ?? 'log'
      // const t = typeof o.time === 'number' ? o.time : Date.now()
      // const out = { ...o, time: isoJst(new Date(t)) }
      // console[method](JSON.stringify(out))
      // 追加(レベル→スタイル対応)
      const STYLE_FOR: Record<LevelName, string> = {
        fatal: 'background:#B00020;color:#fff;padding:0 6px;border-radius:4px',
        error: 'background:#D32F2F;color:#fff;padding:0 6px;border-radius:4px',
        warn:  'background:#F9A825;color:#000;padding:0 6px;border-radius:4px',
        info:  'background:#1976D2;color:#fff;padding:0 6px;border-radius:4px',
        debug: 'background:#455A64;color:#fff;padding:0 6px;border-radius:4px',
        trace: 'background:#9E9E9E;color:#000;padding:0 6px;border-radius:4px',
      }
 
      // …browser: { asObject: true, write(o) { … } } の中だけ置き換え
      const levelName = LABEL[o.level] ?? 'info'
      const method    = CONSOLE_FOR[levelName] ?? 'log'
      const style     = STYLE_FOR[levelName]
      const t         = typeof o.time === 'number' ? new Date(o.time) : new Date()
      const jst       = isoJst(t)
 
      // 開発しやすいよう、見出しは色付きテキスト、データは展開できるオブジェクトで出す
      const header = `%c${levelName.toUpperCase()}%c ${jst} %c${SCHEMA}`
      console[method](
        header,
        style,                 // 1つ目の %c(レベルバッジ)
        '',                    // 2つ目の %c(デフォルトに戻す)
        'color:#888',          // 3つ目の %c(schemaを薄色に)
        o.payload ?? o         // 展開可能なオブジェクト(payloadが無ければ元オブジェクト)
      )
 
      // 2) リモート送信(warn 以上など)
      if (!remoteUrl) return
      if (o.level < LV[remoteLevel]) return
 
      // 必須: level, schema, message, timestamp(JST)
      // 任意: payload(undefined のときはキー自体を送らない)
      const base = {
        level: LABEL[o.level] ?? 'info',
        schema: SCHEMA,
        message: o.msg || '',     // ★ 必須
        timestamp: isoJst(),      // ★ 必須(JST)
      } as Record<string, unknown>
 
      if (o.payload !== undefined) base.payload = o.payload
 
      const body = JSON.stringify(base)
 
      if (navigator.sendBeacon) {
        const blob = new Blob([body], { type: 'application/json' })
        navigator.sendBeacon(remoteUrl, blob)
      } else {
        fetch(remoteUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          keepalive: true,
          credentials: 'omit',    // ★ CORSで資格情報を要求しない
          body,
        }).catch(() => { /* no-op */ })
      }
    },
  },
}
 
const _pino = pino(opts)
 
/* ===== アプリ向けラッパー:logger.debug(message, payload?) ===== */
export interface AppLogger {
  trace(message: string, payload?: unknown): void
  debug(message: string, payload?: unknown): void
  info (message: string, payload?: unknown): void
  warn (message: string, payload?: unknown): void
  error(message: string, payload?: unknown): void
  fatal(message: string, payload?: unknown): void
  child(bindings: Record<string, unknown>): AppLogger
}
 
function emit(level: LevelName, base: any, message: string, payload?: unknown) {
  // payload 未定義ならキー自体を作らない(undefined を送らない)
  const obj = payload === undefined ? {} : { payload }
  base[level](obj, message) // pino は (obj, msg)
}
 
function wrap(p: any): AppLogger {
  return {
    trace: (m, pl) => emit('trace', p, m, pl),
    debug: (m, pl) => emit('debug', p, m, pl),
    info:  (m, pl) => emit('info',  p, m, pl),
    warn:  (m, pl) => emit('warn',  p, m, pl),
    error: (m, pl) => emit('error', p, m, pl),
    fatal: (m, pl) => emit('fatal', p, m, pl),
    child(bindings) { return wrap(p.child(bindings)) },
  }
}
 
const logger = wrap(_pino)
 
/* ===== 公開API ===== */
export function useLogger(bindings?: Record<string, unknown>): AppLogger {
  return bindings ? logger.child(bindings) : logger
}
 
export function useSchemaLogger(extra?: Record<string, unknown>): AppLogger {
  return useLogger({ schema: SCHEMA, ...(extra ?? {}) })
}
 
export default logger