diff --git a/.config/docker_example.yml b/.config/docker_example.yml index d4d5118f1b..40ad17fd10 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -147,6 +147,12 @@ id: 'aid' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Cloud Logging +#cloudLogging: +# projectId: example-project-id +# saKeyPath: /path/to/service-account-key.json +# logName: misskey + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index e3b61ed788..1917037134 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -151,6 +151,12 @@ id: 'aid' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Cloud Logging +#cloudLogging: +# projectId: example-project-id +# saKeyPath: /path/to/service-account-key.json +# logName: misskey + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 4b612ad11c..19723d61fd 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -147,6 +147,12 @@ id: 'aid' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Cloud Logging +#cloudLogging: +# projectId: example-project-id +# saKeyPath: /path/to/service-account-key.json +# logName: misskey + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/chart/files/default.yml b/chart/files/default.yml index a9e4c579fb..d7c28a8feb 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -167,6 +167,12 @@ id: "aid" # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Cloud Logging +#cloudLogging: +# projectId: example-project-id +# saKeyPath: /path/to/service-account-key.json +# logName: misskey + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/packages/backend/package.json b/packages/backend/package.json index 139d55d440..56d00f7106 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -65,6 +65,7 @@ "@fastify/multipart": "7.6.0", "@fastify/static": "6.10.2", "@fastify/view": "7.4.1", + "@google-cloud/logging": "^10.5.0", "@google-cloud/translate": "^7.2.1", "@nestjs/common": "9.4.2", "@nestjs/core": "9.4.2", @@ -147,6 +148,7 @@ "slacc": "0.0.9", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", + "strip-ansi": "^7.1.0", "summaly": "github:misskey-dev/summaly", "systeminformation": "5.17.16", "tinycolor2": "1.6.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c5cd9563d6..60f2d5bf9f 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -90,6 +90,12 @@ export type Source = { deliverJobMaxAttempts?: number; inboxJobMaxAttempts?: number; + cloudLogging?: { + projectId: string; + saKeyPath: string; + logName?: string; + } + mediaProxy?: string; proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 441c254f48..826c6a95f2 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -7,14 +7,18 @@ import type { KEYWORD } from 'color-convert/conversions'; @Injectable() export class LoggerService { + private cloudLogging; constructor( @Inject(DI.config) private config: Config, ) { + if (this.config.cloudLogging) { + this.cloudLogging = this.config.cloudLogging; + } } @bindThis public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { - return new Logger(domain, color, store); + return new Logger(domain, color, store, this.cloudLogging); } } diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index 91039098f1..e452c4e560 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -1,7 +1,10 @@ import cluster from 'node:cluster'; +import util from 'util'; import chalk from 'chalk'; import { default as convertColor } from 'color-convert'; import { format as dateFormat } from 'date-fns'; +import { Logging } from '@google-cloud/logging'; +import stripAnsi from 'strip-ansi'; import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; import type { KEYWORD } from 'color-convert/conversions'; @@ -12,18 +15,21 @@ type Context = { }; type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; +type CloudLogging = any | undefined; export default class Logger { private context: Context; private parentLogger: Logger | null = null; private store: boolean; + private clConfig?: CloudLogging; - constructor(context: string, color?: KEYWORD, store = true) { + constructor(context: string, color?: KEYWORD, store = true, clConfig?: CloudLogging) { this.context = { name: context, color: color, }; this.store = store; + this.clConfig = clConfig; } @bindThis @@ -44,7 +50,8 @@ export default class Logger { return; } - const time = dateFormat(new Date(), 'HH:mm:ss'); + const timestamp = new Date(); + const time = dateFormat(timestamp, 'HH:mm:ss'); const worker = cluster.isPrimary ? '*' : cluster.worker!.id; const l = level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') : @@ -66,7 +73,43 @@ export default class Logger { if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; console.log(important ? chalk.bold(log) : log); - if (level === 'error' && data) console.log(data); + if (level === 'error' && data) { + console.log(data); + this.writeCloudLogging(level, log, timestamp, data); + } else { + this.writeCloudLogging(level, log, timestamp, null); + } + } + + private async writeCloudLogging(level: Level, message: string, time: Date, data?: Record | null) { + if (!this.clConfig) return; + if (!this.clConfig.projectId || !this.clConfig.saKeyPath) return; + + let lv = level; + if (level === 'success') lv = 'info'; + + const projectId = this.clConfig.projectId; + const logging = new Logging({ projectId: projectId, keyFilename: this.clConfig.saKeyPath }); + const logName = this.clConfig.logName ?? 'misskey'; + const log = logging.log(logName); + const logMessage = stripAnsi(message); + + const metadata = { + severity: lv.toUpperCase(), + resource: { + type: 'global', + timestamp: time, + }, + labels: { + name: `${this.context.name}`, + color: `${this.context.color}`, + }, + }; + + const dataString = data ? '\n' + util.inspect(data, { depth: null }) : ''; + const entry = log.entry(metadata, logMessage + dataString); + + await log.write(entry); } @bindThis diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0419e293d6..e5f3491cae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@fastify/view': specifier: 7.4.1 version: 7.4.1 + '@google-cloud/logging': + specifier: ^10.5.0 + version: 10.5.0 '@google-cloud/translate': specifier: ^7.2.1 version: 7.2.1 @@ -356,6 +359,9 @@ importers: stringz: specifier: 2.1.0 version: 2.1.0 + strip-ansi: + specifier: ^7.1.0 + version: 7.1.0 summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/77dd5654bb82280b38c1f50e51a771c33f3df503 @@ -4934,6 +4940,38 @@ packages: - supports-color dev: false + /@google-cloud/logging@10.5.0: + resolution: {integrity: sha512-XmlNs6B8lDZvFwFB5M55g9ch873AA2rXSuFOczQ3FOAzuyd/qksf18suFJfcrLMu8lYSr3SQhTE45FlXz4p9pg==} + engines: {node: '>=12.0.0'} + dependencies: + '@google-cloud/common': 4.0.3 + '@google-cloud/paginator': 4.0.1 + '@google-cloud/projectify': 3.0.0 + '@google-cloud/promisify': 3.0.1 + arrify: 2.0.1 + dot-prop: 6.0.1 + eventid: 2.0.1 + extend: 3.0.2 + gcp-metadata: 4.3.1 + google-auth-library: 8.8.0 + google-gax: 3.6.0 + on-finished: 2.4.1 + pumpify: 2.0.1 + stream-events: 1.0.5 + uuid: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@google-cloud/paginator@4.0.1: + resolution: {integrity: sha512-6G1ui6bWhNyHjmbYwavdN7mpVPRBtyDg/bfqBTAlwr413On2TnFNfDxc9UhTJctkgoCDgQXEKiRPLPR9USlkbQ==} + engines: {node: '>=12.0.0'} + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + dev: false + /@google-cloud/projectify@3.0.0: resolution: {integrity: sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==} engines: {node: '>=12.0.0'} @@ -9095,6 +9133,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + /ansi-styles@2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -11549,6 +11592,13 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 + /dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dependencies: + is-obj: 2.0.0 + dev: false + /dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -11616,7 +11666,6 @@ packages: /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: true /ejs@3.1.8: resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} @@ -12328,6 +12377,13 @@ packages: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: false + /eventid@2.0.1: + resolution: {integrity: sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==} + engines: {node: '>=10'} + dependencies: + uuid: 8.3.2 + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -13174,6 +13230,20 @@ packages: wide-align: 1.1.5 dev: false + /gaxios@4.3.3: + resolution: {integrity: sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==} + engines: {node: '>=10'} + dependencies: + abort-controller: 3.0.0 + extend: 3.0.2 + https-proxy-agent: 5.0.1 + is-stream: 2.0.1 + node-fetch: 2.6.11 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gaxios@5.1.0: resolution: {integrity: sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A==} engines: {node: '>=12'} @@ -13187,6 +13257,17 @@ packages: - supports-color dev: false + /gcp-metadata@4.3.1: + resolution: {integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==} + engines: {node: '>=10'} + dependencies: + gaxios: 4.3.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /gcp-metadata@5.2.0: resolution: {integrity: sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==} engines: {node: '>=12'} @@ -14521,6 +14602,11 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: false + /is-path-cwd@2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} @@ -17196,7 +17282,6 @@ packages: engines: {node: '>= 0.8'} dependencies: ee-first: 1.1.1 - dev: true /on-headers@1.0.2: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} @@ -18514,6 +18599,14 @@ packages: inherits: 2.0.4 pump: 2.0.1 + /pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + dependencies: + duplexify: 4.1.2 + inherits: 2.0.4 + pump: 3.0.0 + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -20349,6 +20442,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + /strip-bom@2.0.0: resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} engines: {node: '>=0.10.0'}