How do you capture logs and write them to `stderr`?

I have the following line Code.eval_string(code)
which gives me the result of the inputted code and I write it to stdout

I want to capture any logs as well and write them to stderr but I’m not sure how to do that ? is it by using ExUnit ?

I am aware of the security implications

1 Like

Can you give one full example on what you are trying to achieve, please? It will be easier to help you.

To capture logs you can mark the pid with a special metadata and then log out only logs that contain that metadata.

so basically my script outputs the result of Code.eval_string in json format.

But when I run the following:

mix run script.exs 'IO.puts("foobar")'

I get:

foobar
{"result":"ok"}

I would like to catch foobar as well, not ok only.

Here is a way to do it. Keep in mind it shouldn’t’ be used in actual systems, as there is a race condition here since you won’t have a stderr registered for a brief second. It should be fine for scripts like yours though:

iex(1)> Process.unregister(:standard_error)
true
iex(2)> {:ok, device} = StringIO.open("")
{:ok, #PID<0.109.0>}
iex(3)> Process.register(device, :standard_error)
true
iex(4)> IO.warn "nothing will show"
:ok
4 Likes

@josevalim is there a way to turn off warnings like
warning: variable "a" does not exist and is being expanded to "a()", please use parentheses to remove the ambiguity or change the variable name nofile:1

I’m trying to json encode it and then write it to stderr, but I’m only able to write it as it is without encoding. any other ideas if turning it off isn’t an option ?

The code above should hide the warnings, exactly as you asked. Without the code above:

x(1)> Code.eval_string("a")
warning: variable "a" does not exist and is being expanded to "a()", please use parentheses to remove the ambiguity or change the variable name
  nofile:1

** (CompileError) nofile:1: undefined function a/0
    (elixir) lib/code.ex:232: Code.eval_string/3
    (stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
    (iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/3

With the code above (notice no warnings):

iex(1)> Process.unregister(:standard_error)
true
iex(2)> {:ok, device} = StringIO.open("")
{:ok, #PID<0.107.0>}
iex(3)> Process.register(device, :standard_error)
true
iex(4)> Code.eval_string("a")
** (CompileError) nofile:1: undefined function a/0
    (elixir) lib/code.ex:232: Code.eval_string/3
    (stdlib) erl_eval.erl:677: :erl_eval.do_apply/6
    (iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/3

yea that is exactly why I asked again, cause I think it doesn’t answer my original question in this post which was, how do I retrieve the output of IO.puts ? as I described in this post

so basically, how do I capture the output of the following line Code.eval_string('IO.puts("test")')

@josevalim would ExUnit do that job ?

Did you try it?

The warning doesn’t appear, but neither does the undefined function. I get nothing :open_mouth:

Can you show a full example of what you have at this point?

      Process.unregister(:standard_error)
      {:ok, device} = StringIO.open("")
      Process.register(device, :standard_error)
      {result, _} = Code.eval_string(code)

This is what I have in my .exs
code is what I get from the user, so for example mix run script.exs 1+1

I usually get the result in json, but after adding the lines above I don’t get anything returned.

I would expect the output to be in the device you registered. With a quick check of hexdocs I note StringIO.contents/1 will return a 2-tuple of input/output buffers. Have you tried that?

I get eof. I think what you mentioned is true of I IO.write first, which I haven’t done…

Is there a way to actually encode the error to json, instead of working on hiding it ?

Are you writing the results to stderr or stdio?

The results to stdio and when there’s an error (using try-rescue) then I write to stderr. after adding the lines you gave me to the .exs I don’t get anything back when there’s any type of error. normal results still work fine.

Yes, nothing is written to stderr because you have replaced the stderr device by one that captures it. You can do the reverse work and register the :standard_error back to the original process. I will leave this as an exercise. :slight_smile:

2 Likes

:stderr is just an alias to :standard_error. Check the IO module docs for more info.

@sadcad after reading this thread two times it seems to me that all information you need has been included here already.

One last piece of the puzzle that you might be missing still is that you can save the original device registered as :standard_error and use it later to either write to it directly or reregister it once you’re done with the eval and write the captured output after. Something like:

iex(2)> original_stderr = Process.whereis(:standard_error)
#PID<0.59.0>
iex(3)> Process.unregister(:standard_error)
true
iex(4)> {:ok, dev} = StringIO.open("")
{:ok, #PID<0.184.0>}
iex(5)> Process.register(dev, :standard_error)
true
iex(6)> IO.puts :stderr, "Oops!"
:ok
iex(7)> captured = StringIO.flush(dev)
"Oops!\n"
iex(8)> IO.write(original_stderr, captured)
Oops!
:ok
1 Like