Repository: census-instrumentation/opencensus-python (python)

Issue:
ProbabilitySampler does not work with nested spans
## Description
When `ProbabilitySampler` decides not to sample, the following fails with `AttributeError: 'NoneType' object has no attribute 'span'`:

```python
with tracer.span(name='span1') as span1:
    do_something_to_trace()
    with span1.span(name='span1_child1') as span1_child1:
        do_something_to_trace()
```

Of course, since if the sampler decides not to sample, then the tracer will be replaced with `NoopTracer` here: https://github.com/census-instrumentation/opencensus-python/blob/cfcfda584ba984e6f0e89684120c1ea94271b2c9/opencensus/trace/tracer.py#L88 . As a consequence, spans are managed by `NullContextManager`s: https://github.com/census-instrumentation/opencensus-python/blob/ad75bf58fc297eeb2979e3973c09907772cb7acc/opencensus/trace/tracers/noop_tracer.py#L48. When that `ContextManager` is entered, nothing is bound to the `with` expression because `__enter__` does not return (returns `None`) here: https://github.com/census-instrumentation/opencensus-python/blob/cfcfda584ba984e6f0e89684120c1ea94271b2c9/opencensus/trace/tracers/base.py#L74-L76  

So this is why in the example `span1` is `None` if the sampler decides not to sample. 

## Environment
Python version: 3.5.1 (on CentOS 7 based Docker image)

pip freeze:
```
cachetools==2.0.1
certifi==2018.1.18
chardet==3.0.4
dill==0.2.7.1
future==0.16.0
google-api-core==0.1.4
google-auth==1.4.1
google-cloud-core==0.28.1
google-cloud-trace==0.17.0
google-gax==0.15.16
googleapis-common-protos==1.5.3
grpcio==1.11.0
grpcio-tools==1.11.0
idna==2.6
opencensus==0.1.4
ply==3.8
protobuf==3.5.2.post1
pyasn1==0.4.2
pyasn1-modules==0.2.1
pytz==2018.4
requests==2.18.4
rsa==3.4.2
six==1.11.0
urllib3==1.22
```

## Proposal
I am really not that familiar with the code, but I would probably let the exporters know about the sampling decision in `SpanData`: https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/span_data.py#L19 and move any logic that depends on the sampling decision into the base exporter. Not sure if this is the best approach though. Another way would be just to let `NullContextManager` bind to `Span` objects that implement the same interface, but they may have a more specific type that indicates they are not to be reported -- then the type check could be done in https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/tracers/context_tracer.py#L99 . It's not trivial, but surely solvable. 

Please do let me know if I missed anything or if you need help fixing this.


Pull request:
Make NullContextManager support nested spans
Fixes #155 

Patch:
diff --git a/opencensus/trace/tracers/base.py b/opencensus/trace/tracers/base.py
index d00a878..340b265 100644
--- a/opencensus/trace/tracers/base.py
+++ b/opencensus/trace/tracers/base.py
@@ -72,7 +72,10 @@ class NullContextManager(object):
         self.span_id = span_id
 
     def __enter__(self):
-        pass  # pragma: NO COVER
+        return self  # pragma: NO COVER
 
     def __exit__(self, exc_type, exc_value, traceback):
         pass  # pragma: NO COVER
+
+    def span(self, name='span'):
+        return NullContextManager()


Test patch:
diff --git a/tests/unit/trace/test_tracer.py b/tests/unit/trace/test_tracer.py
index 33e9afb..82a778d 100644
--- a/tests/unit/trace/test_tracer.py
+++ b/tests/unit/trace/test_tracer.py
@@ -141,7 +141,12 @@ class TestTracer(unittest.TestCase):
         tracer = tracer_module.Tracer(sampler=sampler)
 
         span = tracer.span()
+
+        # Test nested span not sampled
+        child_span = span.span()
+
         assert isinstance(span, base.NullContextManager)
+        assert isinstance(child_span, base.NullContextManager)
 
     def test_span_sampled(self):
         sampler = mock.Mock()