mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 07:25:04 +02:00
Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
@@ -241,6 +241,8 @@ proxyBypassHosts:
|
||||
|
||||
# Log settings
|
||||
# logging:
|
||||
# # ログの出力形式。「json」で1行JSON、未指定時は「pretty」です。
|
||||
# format: pretty
|
||||
# # Minimum level for all loggers. Defaults to info in production and debug otherwise.
|
||||
# level: info
|
||||
# # The longest matching logger domain takes precedence over the global level.
|
||||
|
||||
@@ -474,6 +474,8 @@ proxyBypassHosts:
|
||||
|
||||
# Log settings
|
||||
# logging:
|
||||
# # ログの出力形式。「json」で1行JSON、未指定時は「pretty」です。
|
||||
# format: pretty
|
||||
# # Minimum level for all loggers. Defaults to info in production and debug otherwise.
|
||||
# level: info
|
||||
# # The longest matching logger domain takes precedence over the global level.
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
- Enhance: URLプレビューの結果を内部でキャッシュするように
|
||||
- Enhance: API内部エラーのログに構造化属性と正規化したエラー情報を付与し、認証情報を自動的に秘匿するように(従来形式の表示は維持)
|
||||
- Enhance: ログ全体の既定levelとlogger domainごとの出力levelを設定できるように
|
||||
- Enhance: バックエンドのログを1行JSON形式で出力できるように
|
||||
- Fix: `/stats` API のレスポンス型が正しくない問題を修正
|
||||
- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正
|
||||
- Fix: Sentry 使用環境下にて、Misskey が発行した SQL クエリが span に含まれない問題を修正
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "Informació"
|
||||
chat: "Xat"
|
||||
directMessage: "Xateja amb aquest usuari"
|
||||
directMessage_short: "Missatge"
|
||||
migrateOldSettings: "Migrar la configuració anterior"
|
||||
migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual."
|
||||
compress: "Comprimir "
|
||||
right: "Dreta"
|
||||
bottom: "A baix "
|
||||
top: "A dalt "
|
||||
embed: "Incrustar"
|
||||
settingsMigrating: "Estem migrant la teva configuració. Si us plau espera un moment... (També pots fer la migració més tard, manualment, anant a Preferències → Altres → Migrar configuració antiga)"
|
||||
readonly: "Només lectura"
|
||||
goToDeck: "Tornar al tauler"
|
||||
federationJobs: "Treballs de federació"
|
||||
|
||||
@@ -1358,14 +1358,11 @@ information: "Über"
|
||||
chat: "Chat"
|
||||
directMessage: "Mit dem Benutzer chatten"
|
||||
directMessage_short: "Nachrichten"
|
||||
migrateOldSettings: "Alte Client-Einstellungen migrieren"
|
||||
migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben."
|
||||
compress: "Komprimieren"
|
||||
right: "Rechts"
|
||||
bottom: "Unten"
|
||||
top: "Oben"
|
||||
embed: "Einbetten"
|
||||
settingsMigrating: "Deine Einstellungen werden gerade migriert. Bitte warte einen Moment... (Du kannst die Einstellungen später auch manuell migrieren, indem du zu Einstellungen → Anderes → Alte Einstellungen migrieren gehst)"
|
||||
readonly: "Nur Lesezugriff"
|
||||
goToDeck: "Zurück zum Deck"
|
||||
federationJobs: "Föderation Jobs"
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "About"
|
||||
chat: "Chat"
|
||||
directMessage: "Chat with user"
|
||||
directMessage_short: "Message"
|
||||
migrateOldSettings: "Migrate old client settings"
|
||||
migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten."
|
||||
compress: "Compress"
|
||||
right: "Right"
|
||||
bottom: "Bottom"
|
||||
top: "Top"
|
||||
embed: "Embed"
|
||||
settingsMigrating: "Settings are being migrated, please wait a moment... (You can also migrate manually later by going to Settings→Others→Migrate old settings)"
|
||||
readonly: "Read only"
|
||||
goToDeck: "Return to Deck"
|
||||
federationJobs: "Federation Jobs"
|
||||
|
||||
@@ -584,7 +584,7 @@ showFixedPostForm: "Mostrar formulario de publicación sobre la línea de tiempo
|
||||
showFixedPostFormInChannel: "Mostrar el formulario de publicación por encima de la cronología (Canales)"
|
||||
withRepliesByDefaultForNewlyFollowed: "Incluir por defecto respuestas de usuarios recién seguidos en la línea de tiempo"
|
||||
newNoteRecived: "Tienes una nota nueva"
|
||||
newNote: "Nueva nota"
|
||||
newNote: "Nuevas notas"
|
||||
sounds: "Sonidos"
|
||||
sound: "Sonidos"
|
||||
notificationSoundSettings: "Configuración del sonido de las notificaciones"
|
||||
@@ -1219,7 +1219,7 @@ keepScreenOn: "Mantener pantalla encendida"
|
||||
verifiedLink: "Propiedad del enlace verificada"
|
||||
notifyNotes: "Notificar nuevas notas"
|
||||
unnotifyNotes: "Dejar de notificar nuevas notas"
|
||||
notifyUsers: "Usuarios que han activado las notificaciones de publicaciones"
|
||||
notifyUsers: "Cuentas con notificaciones activadas"
|
||||
authentication: "Autenticación"
|
||||
authenticationRequiredToContinue: "Por favor, autentifícate para continuar"
|
||||
dateAndTime: "Fecha y hora"
|
||||
@@ -1361,14 +1361,11 @@ information: "Información"
|
||||
chat: "Chat"
|
||||
directMessage: "Chatear"
|
||||
directMessage_short: "Mensajes"
|
||||
migrateOldSettings: "Migrar la configuración anterior"
|
||||
migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual."
|
||||
compress: "Compresión de la imagen"
|
||||
right: "Derecha"
|
||||
bottom: "Abajo"
|
||||
top: "Arriba"
|
||||
embed: "Insertar"
|
||||
settingsMigrating: "La configuración está siendo migrada, por favor espera un momento... (También puedes migrar manualmente más tarde yendo a Ajustes otros migrar configuración antigua"
|
||||
readonly: "Solo Lectura"
|
||||
goToDeck: "Volver al Deck"
|
||||
federationJobs: "Trabajos de Federación"
|
||||
|
||||
@@ -1358,6 +1358,7 @@ _chat:
|
||||
invitations: "Undang"
|
||||
history: "Riwayat obrolan"
|
||||
noHistory: "Tidak ada riwayat"
|
||||
join: "Gabung"
|
||||
members: "Anggota"
|
||||
home: "Beranda"
|
||||
send: "Kirim"
|
||||
@@ -2555,6 +2556,8 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "Gunakan antarmuka sederhana ke halaman yang dituju"
|
||||
usedAsMinWidthWhenFlexible: "Lebar minimum akan digunakan untuk ini ketika opsi \"Atur-otomatis lebar\" dinyalakan"
|
||||
flexible: "Atur-otomatis lebar"
|
||||
_howToUse:
|
||||
settings_title: "Pengaturan UI"
|
||||
_columns:
|
||||
main: "Utama"
|
||||
widgets: "Widget"
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "Informazioni"
|
||||
chat: "Chat"
|
||||
directMessage: "Chattare insieme"
|
||||
directMessage_short: "Messaggio"
|
||||
migrateOldSettings: "Migrare le vecchie impostazioni"
|
||||
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
|
||||
compress: "Compressione"
|
||||
right: "Destra"
|
||||
bottom: "Sotto"
|
||||
top: "Sopra"
|
||||
embed: "Incorporare"
|
||||
settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anche migrare manualmente in un secondo momento, nel menu: Impostazioni → Altro → Migrazione delle impostazioni)"
|
||||
readonly: "Sola lettura"
|
||||
goToDeck: "Torna al Deck"
|
||||
federationJobs: "Coda di federazione"
|
||||
|
||||
@@ -1332,9 +1332,6 @@ preferenceSyncConflictChoiceCancel: "同期の有効化はやめとくわ"
|
||||
postForm: "投稿フォーム"
|
||||
information: "情報"
|
||||
directMessage: "チャットしよか"
|
||||
migrateOldSettings: "旧設定情報をお引っ越し"
|
||||
migrateOldSettings_description: "通常これは自動で行われるはずなんやけど、なんかの理由で上手く移行できへんかったときは手動で移行処理をポチっとできるで。今の設定情報は上書きされるで。"
|
||||
settingsMigrating: "設定を移行しとるで。ちょっと待っとってな... (後で、設定→その他→旧設定情報を移行 で手動で移行することもできるで)"
|
||||
driveAboutTip: "ドライブでは、今までアップロードしたファイルがずらーっと表示されるで。<br>\nノートにファイルをもっかいのっけたり、あとで投稿するファイルをその辺に置いとくこともできるねん。<br>\n<b>ファイルをほかすと、前にそのファイルをのっけた全部の場所(ノート、ページ、アバター、バナー等)からも見えんくなるから気いつけてな。</b><br>\nフォルダを作って整理することもできるで。"
|
||||
turnItOn: "オンにしとこ"
|
||||
turnItOff: "オフでええわ"
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "정보"
|
||||
chat: "채팅"
|
||||
directMessage: "채팅하기"
|
||||
directMessage_short: "메시지"
|
||||
migrateOldSettings: "기존 설정 정보를 이전"
|
||||
migrateOldSettings_description: "보통은 자동으로 이루어지지만, 어떤 이유로 인해 성공적으로 이전이 이루어지지 않는 경우 수동으로 이전을 실행할 수 있습니다. 현재 설정 정보는 덮어쓰게 됩니다."
|
||||
compress: "압축"
|
||||
right: "오른쪽"
|
||||
bottom: "아래"
|
||||
top: "위"
|
||||
embed: "임베드"
|
||||
settingsMigrating: "설정을 이전하는 중입니다. 잠시 기다려주십시오... (나중에 '환경설정 → 기타 → 기존 설정 정보를 이전'에서 수동으로 이전할 수도 있습니다)"
|
||||
readonly: "읽기 전용"
|
||||
goToDeck: "덱으로 돌아가기"
|
||||
federationJobs: "연합 작업"
|
||||
|
||||
@@ -1347,14 +1347,11 @@ information: "Sobre"
|
||||
chat: "Conversas"
|
||||
directMessage: "Conversar com usuário"
|
||||
directMessage_short: "Mensagem"
|
||||
migrateOldSettings: "Migrar configurações antigas de cliente"
|
||||
migrateOldSettings_description: "Isso deve ser feito automaticamente. Caso o processo de migração tenha falhado, você pode acioná-lo manualmente. As informações atuais de migração serão substituídas."
|
||||
compress: "Comprimir"
|
||||
right: "Direita"
|
||||
bottom: "Inferior"
|
||||
top: "Superior"
|
||||
embed: "Embed"
|
||||
settingsMigrating: "Configurações estão sendo migradas, aguarde... (Você pode migrar manualmente em Configurações→Outros→Migrar configurações antigas de cliente)"
|
||||
readonly: "Ler apenas"
|
||||
goToDeck: "Voltar ao Deck"
|
||||
federationJobs: "Tarefas de Federação"
|
||||
|
||||
@@ -1358,14 +1358,11 @@ information: "เกี่ยวกับ"
|
||||
chat: "แชต"
|
||||
directMessage: "แชตเลย"
|
||||
directMessage_short: "ข้อความ"
|
||||
migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า"
|
||||
migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ"
|
||||
compress: "บีบอัด"
|
||||
right: "ขวา"
|
||||
bottom: "ล่าง"
|
||||
top: "บน"
|
||||
embed: "ฝัง"
|
||||
settingsMigrating: "กำลังย้ายการตั้งค่า กรุณารอสักครู่... (สามารถย้ายด้วยตนเองภายหลังได้ที่ การตั้งค่า → อื่นๆ → ย้ายข้อมูลการตั้งค่าเก่า)"
|
||||
readonly: "อ่านได้อย่างเดียว"
|
||||
goToDeck: "กลับไปยังเด็ค"
|
||||
federationJobs: "งานสหพันธ์"
|
||||
|
||||
1
locales/tl-PH.yml
Normal file
1
locales/tl-PH.yml
Normal file
@@ -0,0 +1 @@
|
||||
---
|
||||
@@ -1358,14 +1358,11 @@ information: "Hakkında"
|
||||
chat: "Sohbet"
|
||||
directMessage: "Kullanıcıyla sohbet et"
|
||||
directMessage_short: "Mesaj"
|
||||
migrateOldSettings: "Eski istemci ayarlarını taşıma"
|
||||
migrateOldSettings_description: "Bu işlem otomatik olarak yapılmalıdır, ancak herhangi bir nedenle geçiş başarısız olursa, geçiş işlemini manuel olarak kendin başlatabilirsin. Mevcut yapılandırma bilgileri üzerine yazılacaktır."
|
||||
compress: "Sıkıştır"
|
||||
right: "Sağ"
|
||||
bottom: "Alt"
|
||||
top: "Üst"
|
||||
embed: "Göm"
|
||||
settingsMigrating: "Ayarlar taşınıyor, lütfen bir dakika bekle... (Daha sonra Ayarlar→Diğerler→Eski ayarları taşı seçeneğine giderek manuel olarak da taşıyabilirsin)"
|
||||
readonly: "Sadece okuma"
|
||||
goToDeck: "Güverteye Dön"
|
||||
federationJobs: "Federasyon İşleri"
|
||||
|
||||
@@ -1314,14 +1314,11 @@ information: "Інформація"
|
||||
chat: "Чат"
|
||||
directMessage: "Чат із користувачем"
|
||||
directMessage_short: "Повідомлення"
|
||||
migrateOldSettings: "Перенести минулі налаштування клієнта"
|
||||
migrateOldSettings_description: "Це має відбутися автоматично, але якщо з якоїсь причини перенесення не вдалося, ви можете запустити його вручну. Поточні дані конфігурації буде перезаписано."
|
||||
compress: "Стиснути"
|
||||
right: "Праворуч"
|
||||
bottom: "Зверху"
|
||||
top: "Знизу"
|
||||
embed: "Вбудувати"
|
||||
settingsMigrating: "Налаштування переносяться, зачекайте трохи... (Ви також можете перенести їх вручну пізніше: Налаштування → Інше → Перенести старі налаштування)"
|
||||
readonly: "Лише для читання"
|
||||
goToDeck: "Повернутися до Деки"
|
||||
federationJobs: "Завдання федерації"
|
||||
|
||||
@@ -1219,8 +1219,6 @@ paste: "dán"
|
||||
postForm: "Mẫu đăng"
|
||||
information: "Giới thiệu"
|
||||
chat: "Trò chuyện"
|
||||
migrateOldSettings: "Di chuyển cài đặt cũ"
|
||||
migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn."
|
||||
driveAboutTip: "Trong Drive, danh sách các tệp bạn đã tải lên trước đây sẽ được hiển thị.<br>\nBạn có thể sử dụng lại chúng khi đính kèm vào ghi chú, hoặc tải lên trước các tệp để đăng sau.<br>\n<b>Lưu ý rằng nếu bạn xóa một tệp, tệp đó cũng sẽ biến mất khỏi tất cả những nơi đã sử dụng tệp đó (ghi chú, trang, ảnh đại diện, biểu ngữ, v.v.).</b><br>\nBạn cũng có thể tạo các thư mục để sắp xếp chúng."
|
||||
inMinutes: "phút"
|
||||
inDays: "ngày"
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "关于"
|
||||
chat: "聊天"
|
||||
directMessage: "私信"
|
||||
directMessage_short: "消息"
|
||||
migrateOldSettings: "迁移旧设置信息"
|
||||
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
|
||||
compress: "压缩"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
embed: "嵌入"
|
||||
settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)"
|
||||
readonly: "只读"
|
||||
goToDeck: "返回至 Deck"
|
||||
federationJobs: "联邦作业"
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "關於"
|
||||
chat: "聊天"
|
||||
directMessage: "直接訊息"
|
||||
directMessage_short: "訊息"
|
||||
migrateOldSettings: "遷移舊設定資訊"
|
||||
migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。"
|
||||
compress: "壓縮"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
embed: "嵌入"
|
||||
settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)"
|
||||
readonly: "唯讀"
|
||||
goToDeck: "回到多欄模式"
|
||||
federationJobs: "聯邦通訊作業"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.7.0-alpha.5",
|
||||
"version": "2026.7.0-alpha.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { writeHeapSnapshot } from 'node:v8';
|
||||
import chalk from 'chalk';
|
||||
import Xev from 'xev';
|
||||
import Logger from '@/logger.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { installProcessErrorHandlers } from './process-error-handler.js';
|
||||
import { isShutdownInProgress } from './shutdown-handler.js';
|
||||
import { readyRef } from './ready.js';
|
||||
|
||||
@@ -28,6 +28,8 @@ const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange');
|
||||
const ev = new Xev();
|
||||
|
||||
installProcessErrorHandlers({ logger, quiet: envOption.quiet });
|
||||
|
||||
//#region Events
|
||||
|
||||
// Listen new workers
|
||||
@@ -47,25 +49,11 @@ cluster.on('exit', worker => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the dead worker,
|
||||
// we're not sentimental
|
||||
clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
|
||||
// 終了したワーカーは従来どおり再生成し、表示色は出力処理へ任せます。
|
||||
clusterLogger.error(`[${worker.id}] died :(`);
|
||||
cluster.fork();
|
||||
});
|
||||
|
||||
// Display detail of unhandled promise rejection
|
||||
if (!envOption.quiet) {
|
||||
process.on('unhandledRejection', console.dir);
|
||||
}
|
||||
|
||||
// Display detail of uncaught exception
|
||||
process.on('uncaughtException', err => {
|
||||
try {
|
||||
logger.error(err);
|
||||
console.trace(err);
|
||||
} catch { }
|
||||
});
|
||||
|
||||
// Dying away...
|
||||
process.on('exit', code => {
|
||||
if (isShutdownInProgress()) return;
|
||||
@@ -76,12 +64,10 @@ process.on('exit', code => {
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
if (cluster.isPrimary) {
|
||||
logger.info(`Start main process... pid: ${process.pid}`);
|
||||
const { masterMain } = await import('./master.js');
|
||||
await masterMain();
|
||||
ev.mount();
|
||||
} else if (cluster.isWorker) {
|
||||
logger.info(`Start worker process... pid: ${process.pid}`);
|
||||
const { workerMain } = await import('./worker.js');
|
||||
await workerMain();
|
||||
} else {
|
||||
@@ -89,7 +75,6 @@ if (!envOption.disableClustering) {
|
||||
}
|
||||
} else {
|
||||
// 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
|
||||
logger.info(`Start main process... pid: ${process.pid}`);
|
||||
const { masterMain } = await import('./master.js');
|
||||
await masterMain();
|
||||
ev.mount();
|
||||
|
||||
@@ -12,6 +12,7 @@ import Logger from '@/logger.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { configureLogging, shutdownLogging } from '@/logging/logging-runtime.js';
|
||||
import type { LogFormat } from '@/logging/types.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { initTelemetry, shutdownTelemetry } from '@/core/telemetry/telemetry-registry.js';
|
||||
@@ -23,7 +24,17 @@ const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||
|
||||
const themeColor = chalk.hex('#86b300');
|
||||
|
||||
function greet(props: { version: string }) {
|
||||
/** 起動時の案内を、選択されたログ形式に合わせて出力します。 */
|
||||
function greet(props: { version: string; format: LogFormat }) {
|
||||
if (!envOption.quiet && props.format === 'json') {
|
||||
// JSONモードでは生のコンソール出力を避け、各案内を1件ずつ構造化ログにします。
|
||||
bootLogger.info('Welcome to Misskey!');
|
||||
bootLogger.info(`Misskey v${props.version}`, null, true);
|
||||
bootLogger.info('Misskey is an open-source decentralized microblogging platform.');
|
||||
bootLogger.info('If you like Misskey, please consider donating to support dev. https://misskey-hub.net/docs/donate/');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!envOption.quiet) {
|
||||
//#region Misskey logo
|
||||
const v = `v${props.version}`;
|
||||
@@ -54,7 +65,9 @@ export async function masterMain() {
|
||||
// initialize app
|
||||
try {
|
||||
config = loadConfigBoot();
|
||||
greet({ version: config.version });
|
||||
logger.info(`Start main process... pid: ${process.pid}`);
|
||||
bootLogger.createSubLogger('config').succ('Loaded');
|
||||
greet({ version: config.version, format: config.logging?.format ?? 'pretty' });
|
||||
showEnvironment();
|
||||
await showMachineInfo(bootLogger);
|
||||
showNodejsVersion();
|
||||
@@ -136,6 +149,7 @@ function showNodejsVersion(): void {
|
||||
nodejsLogger.info(`Version ${process.version} detected.`);
|
||||
}
|
||||
|
||||
/** 設定を読み込み、成功時に後続のログ出力形式を適用します。 */
|
||||
function loadConfigBoot(): Config {
|
||||
const configLogger = bootLogger.createSubLogger('config');
|
||||
let config;
|
||||
@@ -154,8 +168,6 @@ function loadConfigBoot(): Config {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
configLogger.succ('Loaded');
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
59
packages/backend/src/boot/process-error-handler.ts
Normal file
59
packages/backend/src/boot/process-error-handler.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { LogWriteInput } from '@/logging/types.js';
|
||||
|
||||
/** プロセス例外をロガーへ渡すために必要な最小の処理対象です。 */
|
||||
export type ProcessErrorHandlerProcess = {
|
||||
on(event: 'unhandledRejection', listener: (reason: unknown) => void): unknown;
|
||||
on(event: 'uncaughtException', listener: (error: Error) => void): unknown;
|
||||
};
|
||||
|
||||
/** 例外記録に必要なロガーの最小インターフェースです。 */
|
||||
export type ProcessErrorHandlerLogger = {
|
||||
write(input: LogWriteInput): void;
|
||||
};
|
||||
|
||||
/** プロセス例外ハンドラーの登録に使う依存関係です。 */
|
||||
export type ProcessErrorHandlerOptions = {
|
||||
readonly process?: ProcessErrorHandlerProcess;
|
||||
readonly logger: ProcessErrorHandlerLogger;
|
||||
readonly quiet: boolean;
|
||||
};
|
||||
|
||||
/** ログ記録の失敗が別の例外を起こさないよう、プロセス異常を安全に記録します。 */
|
||||
function writeProcessError(
|
||||
logger: ProcessErrorHandlerLogger,
|
||||
eventName: 'process.unhandled_rejection' | 'process.uncaught_exception',
|
||||
message: string,
|
||||
error: unknown,
|
||||
): void {
|
||||
try {
|
||||
// 元の値をerrorへ渡し、JSON形式の出力処理で安全な形へ正規化します。
|
||||
logger.write({
|
||||
level: 'error',
|
||||
eventName,
|
||||
message,
|
||||
error,
|
||||
});
|
||||
} catch {
|
||||
// 例外処理中のログ失敗で、さらにプロセスを不安定にしないよう握りつぶします。
|
||||
}
|
||||
}
|
||||
|
||||
/** 未処理のPromise拒否と未捕捉例外を構造化ログへ接続します。 */
|
||||
export function installProcessErrorHandlers(options: ProcessErrorHandlerOptions): void {
|
||||
const processLike: ProcessErrorHandlerProcess = options.process ?? (process as unknown as ProcessErrorHandlerProcess);
|
||||
|
||||
if (!options.quiet) {
|
||||
processLike.on('unhandledRejection', reason => {
|
||||
writeProcessError(options.logger, 'process.unhandled_rejection', 'Unhandled promise rejection', reason);
|
||||
});
|
||||
}
|
||||
|
||||
processLike.on('uncaughtException', error => {
|
||||
writeProcessError(options.logger, 'process.uncaught_exception', 'Uncaught exception', error);
|
||||
});
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export async function workerMain() {
|
||||
try {
|
||||
config = loadConfig();
|
||||
configureLogging(config.logging);
|
||||
logger.info(`Start worker process... pid: ${process.pid}`);
|
||||
} catch (e) {
|
||||
bootLogger.error(e instanceof Error ? e : new Error(String(e)), null, true);
|
||||
process.exit(1);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
import type { LogLevelSetting } from './logging/types.js';
|
||||
import type { LogFormat, LogLevelSetting } from './logging/types.js';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
host: string;
|
||||
@@ -133,6 +133,7 @@ type Source = {
|
||||
pidFile: string;
|
||||
|
||||
logging?: {
|
||||
format?: LogFormat;
|
||||
level?: LogLevelSetting;
|
||||
domains?: Record<string, LogLevelSetting> | null;
|
||||
sql?: {
|
||||
@@ -197,6 +198,7 @@ export type Config = {
|
||||
deliverJobMaxAttempts: number | undefined;
|
||||
inboxJobMaxAttempts: number | undefined;
|
||||
logging?: {
|
||||
format?: LogFormat;
|
||||
level?: LogLevelSetting;
|
||||
domains?: Record<string, LogLevelSetting> | null;
|
||||
sql?: {
|
||||
|
||||
71
packages/backend/src/logging/JsonConsoleBackend.ts
Normal file
71
packages/backend/src/logging/JsonConsoleBackend.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { LogBackend } from './LogBackend.js';
|
||||
import type { LogRecord } from './types.js';
|
||||
|
||||
/** JSON形式のログを1行で出力する処理が外部から受け取る依存関係です。 */
|
||||
export type JsonConsoleBackendDependencies = {
|
||||
readonly output: (line: string) => void;
|
||||
};
|
||||
|
||||
/** JSON形式で出力する項目を、運用上安定した形として定義します。 */
|
||||
type JsonLogRecord = {
|
||||
readonly timestamp: string;
|
||||
readonly level: LogRecord['level'];
|
||||
readonly message: string;
|
||||
readonly loggerName: string;
|
||||
readonly eventName?: string;
|
||||
readonly attributes?: LogRecord['attributes'];
|
||||
readonly error?: LogRecord['error'];
|
||||
readonly processId: number;
|
||||
readonly isPrimary: boolean;
|
||||
readonly workerId: number | null;
|
||||
};
|
||||
|
||||
const defaultDependencies: JsonConsoleBackendDependencies = {
|
||||
output: line => console.log(line),
|
||||
};
|
||||
|
||||
/** LogRecordからJSONへ出す項目だけを選び、内部情報を誤って含めないようにします。 */
|
||||
function createJsonLogRecord(record: LogRecord): JsonLogRecord {
|
||||
// 色や旧APIの生データは表示専用の情報なので、機械向け形式へは持ち込みません。
|
||||
return {
|
||||
timestamp: record.timestamp,
|
||||
level: record.level,
|
||||
message: record.message,
|
||||
loggerName: record.loggerName,
|
||||
// 任意項目は値がある場合だけ含め、空の項目を増やさないようにします。
|
||||
...(record.eventName != null ? { eventName: record.eventName } : {}),
|
||||
...(record.attributes != null ? { attributes: record.attributes } : {}),
|
||||
...(record.error != null ? { error: record.error } : {}),
|
||||
// 実行主体の情報は常に出し、ログを横断して検索できる形を保ちます。
|
||||
processId: record.processId,
|
||||
isPrimary: record.isPrimary,
|
||||
workerId: record.workerId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* LogRecordを1行のJSONへ変換し、ログ収集基盤が扱える標準出力へ渡します。
|
||||
* LoggerやLogManagerから出力形式を切り離し、Pretty形式と同じ記録を共有します。
|
||||
*/
|
||||
export class JsonConsoleBackend implements LogBackend {
|
||||
private readonly dependencies: JsonConsoleBackendDependencies;
|
||||
|
||||
/** 出力処理を受け取り、テストや起動環境ごとに出力先を差し替えます。 */
|
||||
constructor(dependencies: Partial<JsonConsoleBackendDependencies> = {}) {
|
||||
this.dependencies = {
|
||||
...defaultDependencies,
|
||||
...dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
/** 1件のログをJSON文字列へ変換し、改行を含まない1回の出力として渡します。 */
|
||||
public write(record: LogRecord): void {
|
||||
// JSON.stringifyが改行などをエスケープするため、1ログ1行の契約を保てます。
|
||||
this.dependencies.output(JSON.stringify(createJsonLogRecord(record)));
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,11 @@
|
||||
|
||||
import { LogManager } from './LogManager.js';
|
||||
import { BootstrapConsoleBackend } from './BootstrapConsoleBackend.js';
|
||||
import { JsonConsoleBackend } from './JsonConsoleBackend.js';
|
||||
import { PrettyConsoleBackend } from './PrettyConsoleBackend.js';
|
||||
import type { LogManagerConfiguration } from './LogManager.js';
|
||||
import type { LogBackend } from './LogBackend.js';
|
||||
import type { LogFormat } from './types.js';
|
||||
|
||||
/**
|
||||
* プロセス内のすべてのLoggerが共有するLogManagerです。
|
||||
@@ -14,13 +17,22 @@ import type { LogManagerConfiguration } from './LogManager.js';
|
||||
*/
|
||||
export const logManager = new LogManager(new BootstrapConsoleBackend());
|
||||
|
||||
/** 設定読込後のlogging設定とPretty backendを適用します。 */
|
||||
export function configureLogging(configuration?: LogManagerConfiguration): void {
|
||||
logManager.configure(configuration);
|
||||
logManager.setBackend(new PrettyConsoleBackend());
|
||||
/** ログ形式を検証し、指定された形式に対応する出力処理を作成します。 */
|
||||
function createLoggingBackend(format: unknown): LogBackend {
|
||||
if (format == null || format === 'pretty') return new PrettyConsoleBackend();
|
||||
if (format === 'json') return new JsonConsoleBackend();
|
||||
throw new Error('logging.format must be either pretty or json');
|
||||
}
|
||||
|
||||
/** プロセス終了前に現在のlogging backendをflushして閉じます。 */
|
||||
/** 起動時のログ設定を適用し、選択した出力処理へ切り替えます。 */
|
||||
export function configureLogging(configuration?: LogManagerConfiguration & { readonly format?: LogFormat }): void {
|
||||
// 出力処理を先に検証し、設定値が不正な場合は現在の出力処理を壊さないようにします。
|
||||
const backend = createLoggingBackend(configuration?.format);
|
||||
logManager.configure(configuration);
|
||||
logManager.setBackend(backend);
|
||||
}
|
||||
|
||||
/** プロセス終了前に現在のログ出力処理を保留分まで書き出して閉じます。 */
|
||||
export function shutdownLogging(): Promise<void> {
|
||||
return logManager.shutdown();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
/** 設定で指定できるログの閾値です。`off`はログイベントのlevelには使用しません。 */
|
||||
export type LogLevelSetting = LogLevel | 'off';
|
||||
|
||||
/** コンソールへ出すログ形式です。未指定時は見やすい形式を使用します。 */
|
||||
export type LogFormat = 'pretty' | 'json';
|
||||
|
||||
/** 正規化後にログ属性として扱えるJSONの値です。 */
|
||||
export type LogAttributeValue =
|
||||
| string
|
||||
|
||||
72
packages/backend/test/unit/boot/process-error-handler.ts
Normal file
72
packages/backend/test/unit/boot/process-error-handler.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import type { LogWriteInput } from '@/logging/types.js';
|
||||
import { installProcessErrorHandlers } from '@/boot/process-error-handler.js';
|
||||
|
||||
type ProcessListener = (...args: unknown[]) => void;
|
||||
|
||||
/** テスト用に、登録されたプロセスイベントを呼び出せる処理対象を作成します。 */
|
||||
function createProcessLike(): {
|
||||
readonly process: { on: (event: string, listener: ProcessListener) => void };
|
||||
readonly listeners: Map<string, ProcessListener>;
|
||||
} {
|
||||
const listeners = new Map<string, ProcessListener>();
|
||||
return {
|
||||
process: {
|
||||
on: (event, listener) => listeners.set(event, listener),
|
||||
},
|
||||
listeners,
|
||||
};
|
||||
}
|
||||
|
||||
describe('installProcessErrorHandlers', () => {
|
||||
test('records unhandled rejections and uncaught exceptions as structured errors', () => {
|
||||
const { process, listeners } = createProcessLike();
|
||||
const write = vi.fn<(input: LogWriteInput) => void>();
|
||||
const rejection = new Error('rejected');
|
||||
const exception = new TypeError('uncaught');
|
||||
|
||||
installProcessErrorHandlers({ process: process as never, logger: { write }, quiet: false });
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
listeners.get('unhandledRejection')!(rejection);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
listeners.get('uncaughtException')!(exception);
|
||||
|
||||
expect(write).toHaveBeenNthCalledWith(1, {
|
||||
level: 'error',
|
||||
eventName: 'process.unhandled_rejection',
|
||||
message: 'Unhandled promise rejection',
|
||||
error: rejection,
|
||||
});
|
||||
expect(write).toHaveBeenNthCalledWith(2, {
|
||||
level: 'error',
|
||||
eventName: 'process.uncaught_exception',
|
||||
message: 'Uncaught exception',
|
||||
error: exception,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not register the unhandled rejection handler in quiet mode', () => {
|
||||
const { process, listeners } = createProcessLike();
|
||||
|
||||
installProcessErrorHandlers({ process: process as never, logger: { write: vi.fn() }, quiet: true });
|
||||
|
||||
expect([...listeners.keys()]).toEqual(['uncaughtException']);
|
||||
});
|
||||
|
||||
test('does not rethrow when the logger fails', () => {
|
||||
const { process, listeners } = createProcessLike();
|
||||
const write = vi.fn(() => {
|
||||
throw new Error('logger failed');
|
||||
});
|
||||
|
||||
installProcessErrorHandlers({ process: process as never, logger: { write }, quiet: false });
|
||||
|
||||
expect(() => listeners.get('unhandledRejection')!(new Error('rejected'))).not.toThrow();
|
||||
expect(() => listeners.get('uncaughtException')!(new Error('uncaught'))).not.toThrow();
|
||||
});
|
||||
});
|
||||
76
packages/backend/test/unit/logging/JsonConsoleBackend.ts
Normal file
76
packages/backend/test/unit/logging/JsonConsoleBackend.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import type { LogRecord } from '@/logging/types.js';
|
||||
import { JsonConsoleBackend } from '@/logging/JsonConsoleBackend.js';
|
||||
|
||||
/** JSON形式のテストで使う共通のログを作成します。 */
|
||||
function createRecord(overrides: Partial<LogRecord> = {}): LogRecord {
|
||||
return {
|
||||
level: 'error',
|
||||
message: 'delivery failed',
|
||||
context: [{ name: 'queue' }, { name: 'deliver' }],
|
||||
timestamp: '2025-01-02T03:04:05.678Z',
|
||||
loggerName: 'queue.deliver',
|
||||
processId: 1234,
|
||||
isPrimary: false,
|
||||
workerId: 7,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('JsonConsoleBackend', () => {
|
||||
test('writes a stable one-line JSON record', () => {
|
||||
const output = vi.fn<(line: string) => void>();
|
||||
const backend = new JsonConsoleBackend({ output });
|
||||
|
||||
backend.write(createRecord({
|
||||
eventName: 'queue.job.failed',
|
||||
attributes: { jobId: '123', attempt: 2 },
|
||||
error: { type: 'TypeError', message: 'broken', stack: 'stack' },
|
||||
}));
|
||||
|
||||
expect(output).toHaveBeenCalledOnce();
|
||||
expect(output.mock.calls[0][0]).toMatchInlineSnapshot(`"{\"timestamp\":\"2025-01-02T03:04:05.678Z\",\"level\":\"error\",\"message\":\"delivery failed\",\"loggerName\":\"queue.deliver\",\"eventName\":\"queue.job.failed\",\"attributes\":{\"jobId\":\"123\",\"attempt\":2},\"error\":{\"type\":\"TypeError\",\"message\":\"broken\",\"stack\":\"stack\"},\"processId\":1234,\"isPrimary\":false,\"workerId\":7}"`);
|
||||
});
|
||||
|
||||
test('omits pretty-only compatibility data and context colors', () => {
|
||||
const output = vi.fn<(line: string) => void>();
|
||||
const backend = new JsonConsoleBackend({ output });
|
||||
const record = createRecord({
|
||||
context: [{ name: 'queue', color: 'red' }],
|
||||
compatibility: {
|
||||
legacyLevel: 'success',
|
||||
important: true,
|
||||
data: { secret: 'must not be written' },
|
||||
},
|
||||
});
|
||||
|
||||
backend.write(record);
|
||||
|
||||
expect(JSON.parse(output.mock.calls[0][0])).toEqual({
|
||||
timestamp: '2025-01-02T03:04:05.678Z',
|
||||
level: 'error',
|
||||
message: 'delivery failed',
|
||||
loggerName: 'queue.deliver',
|
||||
processId: 1234,
|
||||
isPrimary: false,
|
||||
workerId: 7,
|
||||
});
|
||||
});
|
||||
|
||||
test('escapes newlines so each record remains one physical line', () => {
|
||||
const output = vi.fn<(line: string) => void>();
|
||||
const backend = new JsonConsoleBackend({ output });
|
||||
const message = 'first line\nsecond line "quoted"';
|
||||
|
||||
backend.write(createRecord({ message }));
|
||||
|
||||
const line = output.mock.calls[0][0];
|
||||
expect(line).not.toContain('\n');
|
||||
expect(JSON.parse(line).message).toBe(message);
|
||||
});
|
||||
});
|
||||
40
packages/backend/test/unit/logging/logging-runtime.ts
Normal file
40
packages/backend/test/unit/logging/logging-runtime.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { JsonConsoleBackend } from '@/logging/JsonConsoleBackend.js';
|
||||
import { logManager, configureLogging } from '@/logging/logging-runtime.js';
|
||||
import { PrettyConsoleBackend } from '@/logging/PrettyConsoleBackend.js';
|
||||
|
||||
describe('logging-runtime', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('uses Pretty backend when the format is omitted', () => {
|
||||
const setBackend = vi.spyOn(logManager, 'setBackend');
|
||||
|
||||
configureLogging();
|
||||
|
||||
expect(setBackend).toHaveBeenCalledWith(expect.any(PrettyConsoleBackend));
|
||||
});
|
||||
|
||||
test('uses JSON backend when the format is json', () => {
|
||||
const setBackend = vi.spyOn(logManager, 'setBackend');
|
||||
|
||||
configureLogging({ format: 'json' });
|
||||
|
||||
expect(setBackend).toHaveBeenCalledWith(expect.any(JsonConsoleBackend));
|
||||
});
|
||||
|
||||
test('rejects an unknown format before changing logging configuration', () => {
|
||||
const setBackend = vi.spyOn(logManager, 'setBackend');
|
||||
const configure = vi.spyOn(logManager, 'configure');
|
||||
|
||||
expect(() => configureLogging({ format: 'xml' as never })).toThrow('logging.format');
|
||||
expect(setBackend).not.toHaveBeenCalled();
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -795,8 +795,11 @@ function openMenu(ev: PointerEvent) {
|
||||
});
|
||||
|
||||
if (props.content.file != null) {
|
||||
menu.push({ type: 'divider' });
|
||||
menu.push(...getFileMenu(props.content.file));
|
||||
const fileMenu = getFileMenu(props.content.file);
|
||||
if (fileMenu.length > 0) {
|
||||
menu.push({ type: 'divider' });
|
||||
menu.push(...fileMenu);
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menu, (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.7.0-alpha.5",
|
||||
"version": "2026.7.0-alpha.6",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
Reference in New Issue
Block a user