🐛 --output json writes invalid JSON lines when events are logged concurrently
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- go
- Ambito
- observability
Direzione di ricerca
Il bug si trova in consoleWriter.Write in logger/console.go, che codifica ogni evento direttamente in c.out e quindi esegue una scrittura per ogni campo JSON. Leggi questa funzione, poi riproduci il problema con il repro autonomo nella issue oppure con il test del writer contatore chiamato TestConsoleLoggerSingleWritePerEvent. È fatto quando l'encoder scrive in un bytes.Buffer, quel buffer viene scritto una sola volta e go test ./logger/ passa.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
With --output json, cloudflared sometimes writes invalid JSON log lines: one event is cut in pieces and another event, logged at the same time from another goroutine, is written in the middle of it.
The cause is in logger/console.go. consoleWriter.Write re-encodes the event with jsoniter.ConfigCompatibleWithStandardLibrary.NewEncoder(c.out) directly into os.Stderr. That config sorts map keys, and sortKeysMapEncoder calls stream.Write for each key. In json-iterator v1.1.12, Stream.Write writes straight through to the underlying writer. So one log event becomes one write(2) per field, plus a final one for }\n, with no lock between them. Events logged concurrently interleave at field boundaries.
strace of a single event:
write(2, "{\"connIndex\":7", 14) = 14
write(2, ",\"dest\":\"https://example.com/api"..., 51) = 51
write(2, ",\"error\":\"Incoming request ended"..., 60) = 60
write(2, ",\"event\":0", 10) = 10
write(2, ",\"level\":\"error\"", 16) = 16
write(2, ",\"message\":\"Request failed\"", 27) = 27
write(2, ",\"time\":\"2026-10-09T07:19:55Z\"", 35) = 35
write(2, ",\"type\":\"http\"", 14) = 14
write(2, "}\n", 2) = 2
The text output (zerolog.ConsoleWriter) is not affected: it builds the whole line in a buffer and writes it once.
To Reproduce
- Run
cloudflared tunnel --no-autoupdate --loglevel info --output json run(remotely managed tunnel, token auth). - Generate bursts of concurrent errors, e.g. many proxied requests cancelled by clients at the same time (
Incoming request ended abruptly: context canceled). - Parse stderr line by line as JSON: some lines are invalid.
If it's an issue with Cloudflare Tunnel:
4. Tunnel ID: not relevant, the bug is in the logger and reproduces without a tunnel (see below)
5. cloudflared config: no config file, only the command-line flags above
Standalone repro: https://github.com/Sirz3chs/cloudflared-json-log-interleave (runnable with ./check.sh, results also visible in its CI). It uses a verbatim copy of consoleWriter, with json-iterator v1.1.12 and zerolog v1.20.0 as in go.mod:
package main
import (
"bytes"
"fmt"
"io"
"os"
"sync"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// Verbatim copy of logger/console.go consoleWriter.
type consoleWriter struct{ out io.Writer }
func (c *consoleWriter) Write(p []byte) (n int, err error) {
var evt map[string]any
d := json.NewDecoder(bytes.NewReader(p))
d.UseNumber()
if err = d.Decode(&evt); err != nil {
return n, fmt.Errorf("cannot decode event: %s", err)
}
e := json.NewEncoder(c.out)
return len(p), e.Encode(evt)
}
func main() {
log := zerolog.New(&consoleWriter{out: os.Stderr}).With().Timestamp().Logger()
var wg sync.WaitGroup
for g := 0; g < 8; g++ {
wg.Add(1)
go func(g int) {
defer wg.Done()
for i := 0; i < 5000; i++ {
log.Error().Int("connIndex", g).Int("event", 0).Str("dest", "https://example.com/api/x").
Str("error", "Incoming request ended abruptly: context canceled").Str("type", "http").Msg("Request failed")
}
}(g)
}
wg.Wait()
}
$ ./repro 2>&1 >/dev/null | jq -R 'fromjson? // "BAD"' | grep -c '^"BAD"$'
10169 # out of 40000 lines
Expected behavior
Each log event is written as one complete JSON line, whatever the concurrency.
Proposed fix
Encode into a buffer and write once. With this change the repro gives 0 invalid lines and 1 write(2) per event instead of 9. go test ./logger/ passes.
--- a/logger/console.go
+++ b/logger/console.go
@@ -32,6 +36,10 @@ func (c *consoleWriter) Write(p []byte) (n int, err error) {
return n, fmt.Errorf("cannot decode event: %s", err)
}
- e := json.NewEncoder(c.out)
- return len(p), e.Encode(evt)
+ var buf bytes.Buffer
+ if err = json.NewEncoder(&buf).Encode(evt); err != nil {
+ return n, err
+ }
+ _, err = c.out.Write(buf.Bytes())
+ return len(p), err
}
A regression test that asserts a single Write per event (it fails on current master with got 7):
type countingWriter struct {
bytes.Buffer
writes int
}
func (c *countingWriter) Write(p []byte) (int, error) {
c.writes++
return c.Buffer.Write(p)
}
func TestConsoleLoggerSingleWritePerEvent(t *testing.T) {
w := &countingWriter{}
logger := zerolog.New(&consoleWriter{out: w}).With().Timestamp().Logger()
logger.Error().Int("connIndex", 3).Str("error", "context canceled").Str("type", "http").Msg("Request failed")
if w.writes != 1 {
t.Errorf("expected 1 write for the log event, got %d: %s", w.writes, w.String())
}
}
Environment and versions
- OS: Linux, official
cloudflare/cloudflaredcontainer image on Kubernetes (EKS, containerd) - Architecture: not architecture specific (reproduced on amd64 with the repro above)
- Version: 2026.10.0, and the code is unchanged on master since TUN-9371 (2025.6)
Logs and errors
Raw container log (CRI file read by kubectl logs --timestamps), URLs redacted. Line 1 is cut before its last }, an entire second event is written inside it, and the orphan } comes next:
2026-10-09T03:03:26.001181426Z {"connIndex":3,"error":"Incoming request ended abruptly: context canceled","event":1,"ingressRule":6,"level":"error","originService":"http://origin.internal","time":"2026-10-09T03:03:26Z"{"connIndex":3,"dest":"https://example.com/api/cart/…","error":"Incoming request ended abruptly: context canceled","event":0,"ip":"198.41.192.167","level":"error","message":"Request failed","time":"2026-10-09T03:03:26Z","type":"http"}
2026-10-09T03:03:26.001206366Z }
Another shape, where the remaining fields of the first event come out on their own line:
{"connIndex":2,"error":"context canceled","event":0,"ip":"198.41.192.77","level":"error"{"connIndex":3,…full event…}
,"message":"failed to run the datagram handler","time":"…"}
Additional context
In production we see 93 invalid lines in 72 h across 2 tunnels, always during bursts of concurrent errors. Log pipelines that parse JSON end up with unparseable fragments, and the original events can't be fully reconstructed.
- Lingua principale
- Go
- Stelle
- 16k
- Fork
- 1.5k
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di cloudflare/cloudflared
-
🐛 cfRay is missing from proxied request logs (newHTTPLogger discards the zerolog context)Forse già presa @pankajc46 l’ha presa 1 giorno fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
cloudflare/cloudflared#1756 ·
-
🐛 After SIGTERM, `cloudflared tunnel run` uses 100% of one CPU core for the whole graceful-shutdown periodForse già presa @cyphercodes l’ha presa 1 giorno fa. ApertaPriority: Normal Type: Bug
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
cloudflare/cloudflared#1753 · 3 commenti ·
-
tunnel route ip show: --filter-network-is-subset-of sends the superset filterForse già presa @wangyusheng1985 l’ha presa 1 giorno fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 92/100
cloudflare/cloudflared#1750 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
cloudflare/cloudflared#1748 ·
-
🐛 QUIC Hijack() skips the status-written check that HTTP/2 enforcesForse già presa @Asthenia0412 l’ha presa 11 giorni fa. ApertaPriority: Normal Type: Bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
cloudflare/cloudflared#1747 ·
Tutte le issue di cloudflare/cloudflared
Issue simili
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
agent-research agent-review-finding chore
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
jordansmall/spindrift#4922 ·
I maintainer di solito rispondono entro 1 giorno
-
gcsartifact: deleting a missing version returns an errorForse già presa @ktsoator l’ha presa oggi. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 2 giorni
-
govulncheck
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
Change wording for init command success messageForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno