Capture Stderr and redirect it to Stdout in service with goroutine in go

64 views Asked by At

I need to write a service calling a black box function. That function may produce errors pushed to Stderr. I need to intercept them and print them immediately. The code below outlines the general idea.

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

func doMagic(i int) {
    fmt.Fprintf(os.Stderr, "%d stderr\n", i)
}

func main() {
    ch := make(chan struct{})
    var buf bytes.Buffer
    var wr, r *os.File
    i := 0
    go func() {
        for {
            ch <- struct{}{}
            i += 1

            doMagic(i)

            wr.Close()
            size, _ := io.Copy(&buf, r)
            if size != 0 {
                fmt.Printf("Size: %d, Content: %s", size, buf.String())
            } else {
                fmt.Println("Empty")
            }
        }
    }()
    for {
        r, wr, _ = os.Pipe()
        os.Stderr = wr
        <-ch
    }
}

However, it doesn't work as expected. I expect that the first iteration of the "for" loop within the goroutine should print the size and content of the first error message generated by the doMagic functions. Instead, it prints "Empty." Why does this occur, and how can I fix it? How to change the code to print errors from the "DoMagic" function immediately? How can I prevent the creation of a new Pipe and the subsequent closure of its write channel on each loop iteration?

UPD (03-19-2024)

It requires the buffer reset:

...
        size, _ := io.Copy(&buf, r)
        if size != 0 {
            fmt.Printf("Size: %d, Content: %s", size, buf.String())
        } else {
            fmt.Println("Empty")
        }
        buf.Reset()  // reset the buffer
...
1

There are 1 answers

0
Nimrata Randhawa On

Create a pipe to capture os.Stderr. Copy pipe to a buffer in a goroutine. Run the magic function. Cleanup.

func run(i int) {
    r, w, _ := os.Pipe()

    // Capture stderr. Restore on return from the function.
    stderr := os.Stderr
    os.Stderr = w
    defer func() {
        os.Stderr = stderr
    }()

    // Copy catured stderr to a buffer.
    done := make(chan *bytes.Buffer)
    go func() {
        defer r.Close()
        var buf bytes.Buffer
        io.Copy(&buf, r)
        done <- &buf
    }()

    doMagic(i)

    // Cause io.Copy to exit by closing the write end of the pipe.
    w.Close()

    // Wait for goroutine to complete.
    buf := <-done

    fmt.Printf("Size: %d, Content: %s", buf.Len(), buf.String())

}

https://go.dev/play/p/bHwd9X_QWPy

To print immediately, use an io.MultiWriter to write to the original stderr and the buffer:

// Copy catured stderr to a buffer and to the original stderr.
done := make(chan *bytes.Buffer)
go func() {
    defer r.Close()
    var buf bytes.Buffer
    io.Copy(io.MultiWriter(&buf, stderr), r)
    done <- &buf
}()

https://go.dev/play/p/egDsSLuFwOV