◆ マルチプロセスになるので各プロセスがログ追記すると問題が出る?
◆ "a" フラグついてると問題なさそう
  ◆ Linux は OS の仕様的に大丈夫そう
  ◆ Windows は実際に試した限りでは Linux とおなじになってそう
◆ 標準出力もログにするなら master プロセスで open して worker プロセスの stdio に fd 設定がよさそう

Node.js で cluster を使うとマルチプロセスになります
ログ出力を考えたときにマルチプロセスでのファイル書き込みってなんか面倒な気がします

書き込みごとにファイルを開いて閉じるなら オープン時にファイルをロックすればいいかもですがやりたいのは stream にして開きっぱなしです
あとロックするにしても Node.js でファイルのロックって見覚えないなと思って調べてみたら標準機能の fs ではサポートしてないみたいです
npm ライブラリ頼りで そのライブラリでは C++ アドオンで flock をコールしているみたいでした

となると worker プロセスでログを書き込みたいときは master プロセスに送って ログはすべて master プロセスで書き込むとかでしょうか?
適当なロガーを調べてみると cluster モードのサポートで master プロセスが書き込むようになっているものもありました
master プロセスを通さないといけないのは面倒ですし そもそも本当にこれが必要なのか試してみました

worker プロセスで追記

結論からいえば "a" フラグがついてればマルチプロセスでも気にしなくて良さそうでした

worker プロセスを数プロセス起動してそれぞれで

const stream = fs.createWriteStream("./log.txt", { flags: "a" })

のようにログファイルを開いて書き込みを繰り返します

それぞれのプロセスで数千回書き込みを行ってログファイルを確認すると 問題なくすべて書き込めていました
行数とバイト数が想定したものとちゃんと一致しています

また マルチプロセスのせいで

console.log("foobar")

を 2 つのプロセスで実行したときに

fofoobarobar

のように一つの書き込みの途中で別の書き込みが挟まるケースをこれまでにみかけたことがあり心配だったのですがこれもありませんでした

1 回の書き込み量が内部バッファを超えているのと発生するのかと思って 一度の書き込みを 1MB 以上にしてみても特に発生しませんでした
タイミング次第なので偶然今回発生しなかった可能性もあるので 何度か試していたのですが結局一度も発生しませんでした

書き込みを繰り返すだけという発生しやすそうな状況にもかかわらず アプリケーションが使われなくなるまでに出力するであろうデータ量以上に書き込んで問題ないなら気にする必要はなさそうです

a フラグを忘れると

flags の a が追記フラグですが これを忘れた場合は書き込みが混ざる現象が起きました
そもそも a がないと追記じゃないので 後からファイルをオープンしたプロセスが すでにオープンしていたプロセスの書き込みを上書きしていくので正常に書けるはずがないです

OS

一応上に書いた確認は Windows と Linux で行ってます

調べてみたら Linux の場合は OS 側で O_APPEND フラグがついていると書き込みごとにポインタをファイルの最後に持ってくることになってるようです
https://linux.die.net/man/2/open

Node.js のプログラム内部で ユーザが実行した write 処理がそのまま system call の write 1 つに対応してるなら確実に問題ないと言って良さそうです
内部的に適当なサイズごとに分けて system call を呼び出してるならそこで問題は起きそうですけど

Windows の方は探してみてもわかりやすいドキュメントがなく公式ドキュメントもいまいちよくわからないです
FILE_APPEND_DATA を CreateFile でつけるみたいなのは見かけましたが CreateFile のページのオプションにこれが書かれてなかったり

仕様的にはよくわかりませんが 実装的には確認した限り Linux と同じように見えます

標準出力もログしたいので

ここまでの方法で一応ログの問題は解決したはずだったのですが 一部ライブラリは console.log や console.error で標準(エラー)出力にログを出力したりします
例えば Koa だとこれ

特に開発中のデバッグ機能を有効にしてる場合だと いろいろなライブラリが標準(エラー)出力へ出力しています

コンソールで直接実行している場合は別にいいのですが そうでない実行方法だとログファイルへ出力されます
ですがそのログファイルは自作ロガーとは別のファイルなので ログを見るときに複数のファイルをみないといけないですし 時系列的に見づらいです

標準(エラー)出力も自作ロガーで受け取りたいですが console.log を上書きするのも少し抵抗があります
いい方法がないか考えていると master プロセスでファイルをオープンして その fd を worker プロセスの stdio に指定すれば良さそうな気がしました
こうすれば worker プロセスのロガーがファイルを開く必要はなく フォーマットして console.log すれば済みます

master プロセス側でファイルを開いているので master プロセス側で fd に write してログすることもできます
master プロセスでログすることはほとんどないですが エラーなどで worker プロセスが終了したことをログできます

試しに worker を 3 プロセスとして master プロセスと worker プロセスからログを書き込むようにしてみました

const fs = require("fs")
const util = require("util")
const cluster = require("cluster")

let logger = null
const createLogger = (write) => {
const log = (level, ...values) => {
const time = new Date().toJSON()
const body = values.map((x) => util.inspect(x)).join(" ")
return write(`${time} [${level}] ${body}\n`)
}

return {
log,
info: log.bind(null, "INFO"),
warn: log.bind(null, "WARN"),
error: log.bind(null, "ERROR"),
}
}

const logMany = async () => {
for (const i of Array(100).keys()) {
for (const j of Array(5).keys()) {
await logger.info(`${i} ${j} a`)
await logger.warn(`${i} ${j} bb`)
await logger.error(`${i} ${j} ccc`)
}
await sleep(10)
}
await logger.info("DONE")
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

if (cluster.isMaster) {
const file = __dirname + "/log"
fs.existsSync(file) && fs.unlinkSync(file)
const fd = fs.openSync(file, "a")

logger = createLogger(
(str) => util.promisify(fs.write)(fd, "[0] " + str)
)

logger.info("first log")
cluster.setupMaster({ stdio: ["pipe", fd, fd, "ipc"] })
cluster.fork()
cluster.fork()
cluster.fork()

logMany().then(async () => {
await sleep(100)
process.exit()
})
} else {
logger = createLogger(
(str) => process.stdout.write(`[${cluster.worker.id}] ${str}`)
)
logger.info("worker start")

logMany()
}

ログの書き込みは info,warn,error を 5 回書き込んで少しスリープを 100 回繰り返してます
1500 行 × 4 プロセスで 6000 行になるので 結果はログファイルの序盤だけ載せます

[0] 2020-10-19T14:23:28.900Z [INFO] 'first log'
[0] 2020-10-19T14:23:28.989Z [INFO] '0 0 a'
[0] 2020-10-19T14:23:29.003Z [WARN] '0 0 bb'
[0] 2020-10-19T14:23:29.005Z [ERROR] '0 0 ccc'
[0] 2020-10-19T14:23:29.017Z [INFO] '0 1 a'
[0] 2020-10-19T14:23:29.023Z [WARN] '0 1 bb'
[0] 2020-10-19T14:23:29.024Z [ERROR] '0 1 ccc'
[0] 2020-10-19T14:23:29.024Z [INFO] '0 2 a'
[0] 2020-10-19T14:23:29.024Z [WARN] '0 2 bb'
[0] 2020-10-19T14:23:29.025Z [ERROR] '0 2 ccc'
[0] 2020-10-19T14:23:29.025Z [INFO] '0 3 a'
[0] 2020-10-19T14:23:29.025Z [WARN] '0 3 bb'
[0] 2020-10-19T14:23:29.025Z [ERROR] '0 3 ccc'
[0] 2020-10-19T14:23:29.026Z [INFO] '0 4 a'
[0] 2020-10-19T14:23:29.026Z [WARN] '0 4 bb'
[0] 2020-10-19T14:23:29.026Z [ERROR] '0 4 ccc'
[1] 2020-10-19T14:23:29.024Z [INFO] 'worker start'
[0] 2020-10-19T14:23:29.037Z [INFO] '1 0 a'
[0] 2020-10-19T14:23:29.040Z [WARN] '1 0 bb'
[0] 2020-10-19T14:23:29.040Z [ERROR] '1 0 ccc'
[0] 2020-10-19T14:23:29.040Z [INFO] '1 1 a'
[1] 2020-10-19T14:23:29.040Z [INFO] '0 0 a'
[0] 2020-10-19T14:23:29.040Z [WARN] '1 1 bb'
[1] 2020-10-19T14:23:29.043Z [WARN] '0 0 bb'
[1] 2020-10-19T14:23:29.043Z [ERROR] '0 0 ccc'
[1] 2020-10-19T14:23:29.043Z [INFO] '0 1 a'
[1] 2020-10-19T14:23:29.043Z [WARN] '0 1 bb'
[1] 2020-10-19T14:23:29.043Z [ERROR] '0 1 ccc'
[1] 2020-10-19T14:23:29.043Z [INFO] '0 2 a'
[1] 2020-10-19T14:23:29.043Z [WARN] '0 2 bb'
[1] 2020-10-19T14:23:29.043Z [ERROR] '0 2 ccc'
[1] 2020-10-19T14:23:29.044Z [INFO] '0 3 a'
[1] 2020-10-19T14:23:29.044Z [WARN] '0 3 bb'
[1] 2020-10-19T14:23:29.044Z [ERROR] '0 3 ccc'
[1] 2020-10-19T14:23:29.044Z [INFO] '0 4 a'
[1] 2020-10-19T14:23:29.044Z [WARN] '0 4 bb'
[1] 2020-10-19T14:23:29.044Z [ERROR] '0 4 ccc'
[0] 2020-10-19T14:23:29.040Z [ERROR] '1 1 ccc'
[2] 2020-10-19T14:23:29.046Z [INFO] 'worker start'
[2] 2020-10-19T14:23:29.050Z [INFO] '0 0 a'
...

最初の数字が worker id で 0 は master プロセスです
載せた範囲では まだ 3 つめの worker プロセスは開始できてませんが 最終的にはすべての行が正常に書き込まれました
fd を渡して worker プロセスのログも 1 つのログファイルにまとまりました
最終的に行数も一致して おかしくなってる行もなかったです

cluster を使ったマルチプロセスでもログの心配は特にしなくて大丈夫そうです