Repository: open-telemetry/opentelemetry-python

Issue:
Custom output function in console exporter
What do folks think of making the output function used by `ConsoleSpanExporter.export` customizable? Currently it uses `print(span)` but there are situations where a user may want to perform a transform on `span` before outputting it, or to output another data structure entirely.

To be more concrete, we currently have downstream infrastructure that requires our telemetry data to be in a specific dictionary format when written to stdout. We'd like to still use the `ConsoleSpanExporter` to write to stdout, but need to convert the `span` to our `dict` object before doing so.

Currently we are addressing this by writing a custom exporter class that inherits from `SpanExporter` and writes our custom data to stdout in its `export` method. Another solution though would be to allow `ConsoleSpanExporter` to take a keyword argument to its `__init__` method that would default to `print` but would also allow custom output functions. It would then use the passed function in `ConsoleSpanExporter.export` to output to stdout.

Do folks see any value in adding this functionality here, rather than requiring users who want to do custom console output to write their own exporter classes? Also happy to write up the proposed solution as a PR if folks think that would be helpful.



Discussion:
I see a lot of value in this! In fact, some time ago I had modified the exporter in place to print the output in a dictionary/JSON-like output where the parent-child relationship of spans could be displayed as they were nested.

I think it is better to leave `ConsoleSpanExporter` as it is now and instead do as you do by inheriting from `SpanExporter`. Even better, we can define an `ABC` for exporters, then use entry points to allow new exporters to plug in, etc.

Sure, open a PR with your code, please :+1: 
Hey @AntonKueltz, welcome to the project.

I agree wth making `SpanExporter` an interface, and in general I think this is the right approach for custom exporters. But in this particular case I think there's a good argument that this could have been built into `ConsoleSpanExporter` from the beginning. Something like:

```python
class ConsoleSpanExporter(SpanExporter):
    """Implementation of :class:`SpanExporter` that prints spans to the
    console.

    This class can be used for diagnostic purposes. It prints the exported
    spans to the console STDOUT.
    """

    def __init__(
        self,
        out: typing.IO = sys.stdout,
        munge: typing.Callable[[Span], str] = str,
    ):
        self.out = out
        self.munge = format

    def export(self, spans: typing.Sequence[Span]) -> SpanExportResult:
        for span in spans:
            self.out.write(self.munge(span))
        return SpanExportResult.SUCCESS
```

It's a pretty short diff, and doesn't affect the default behavior of the exporter:

```diff
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
index aa8efac3..d64d9323 100644
--- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
@@ -14,6 +14,7 @@

 import collections
 import logging
+import sys
 import threading
 import typing
 from enum import Enum
@@ -268,7 +269,15 @@ class ConsoleSpanExporter(SpanExporter):
     spans to the console STDOUT.
     """

+    def __init__(
+        self,
+        out: typing.IO = sys.stdout,
+        munge: typing.Callable[[Span], str] = str,
+    ):
+        self.out = out
+        self.munge = format
+
     def export(self, spans: typing.Sequence[Span]) -> SpanExportResult:
         for span in spans:
-            print(span)
+            self.out.write(self.munge(span))
         return SpanExportResult.SUCCESS
```

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py
@@ -14,6 +14,7 @@
 
 import collections
 import logging
+import sys
 import threading
 import typing
 from enum import Enum
@@ -266,7 +267,15 @@ class ConsoleSpanExporter(SpanExporter):
     spans to the console STDOUT.
     """
 
+    def __init__(
+        self,
+        out: typing.IO = sys.stdout,
+        formatter: typing.Callable[[Span], str] = str,
+    ):
+        self.out = out
+        self.formatter = formatter
+
     def export(self, spans: typing.Sequence[Span]) -> SpanExportResult:
         for span in spans:
-            print(span)
+            self.out.write(self.formatter(span))
         return SpanExportResult.SUCCESS


Test patch:
diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py
--- a/opentelemetry-sdk/tests/trace/export/test_export.py
+++ b/opentelemetry-sdk/tests/trace/export/test_export.py
@@ -276,3 +276,31 @@ def test_batch_span_processor_parameters(self):
             max_queue_size=256,
             max_export_batch_size=512,
         )
+
+
+class TestConsoleSpanExporter(unittest.TestCase):
+    def test_export(self):  # pylint: disable=no-self-use
+        """Check that the console exporter prints spans."""
+        exporter = export.ConsoleSpanExporter()
+
+        # Mocking stdout interferes with debugging and test reporting, mock on
+        # the exporter instance instead.
+        span = trace.Span("span name", mock.Mock())
+        with mock.patch.object(exporter, "out") as mock_stdout:
+            exporter.export([span])
+        mock_stdout.write.assert_called_once_with(str(span))
+        self.assertEqual(mock_stdout.write.call_count, 1)
+
+    def test_export_custom(self):  # pylint: disable=no-self-use
+        """Check that console exporter uses custom io, formatter."""
+        mock_span_str = mock.Mock(str)
+
+        def formatter(span):  # pylint: disable=unused-argument
+            return mock_span_str
+
+        mock_stdout = mock.Mock()
+        exporter = export.ConsoleSpanExporter(
+            out=mock_stdout, formatter=formatter
+        )
+        exporter.export([trace.Span("span name", mock.Mock())])
+        mock_stdout.write.assert_called_once_with(mock_span_str)