-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add stackdriver trace exporter #288
Conversation
TODO: - add GCP/AWS/Azure labels if planned - increase test coverage Test on: - examples/client.py and examples/server.py - https://github.com/open-telemetry/opentelemetry-go/tree/master/example/http-stackdriver
Codecov Report
@@ Coverage Diff @@
## master #288 +/- ##
==========================================
- Coverage 85.66% 84.16% -1.51%
==========================================
Files 33 35 +2
Lines 1612 1756 +144
Branches 180 206 +26
==========================================
+ Hits 1381 1478 +97
- Misses 185 215 +30
- Partials 46 63 +17
Continue to review full report at Codecov.
|
Should this exporter be outside of the main opentelemetry repo? |
Are we going to move them out now? Not sure if there's a guideline to follow. |
This is currently being discussed at #272 and it looks like yes, vendor-specific exporters should not be living in the main Python repository. |
I think it's fine to have two PRs here: one to create the exporter and another to move in into a separate repo. We need the same reviewers in any case, and doing it this way makes it easier to write the exporter in the same style as the others, use the same style checks and CI, etc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The direction looks good, but I've got a few blocking comments on the exporter and I think the tests need some work still.
I reviewed the code, but haven't tested against stackdriver yet. Have you tried sending traces to stackdriver? How did you test this?
It would also be helpful to list the features of the OC exporter (besides resource detection) that aren't implemented here to help reviewers compare the two.
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/__init__.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/tests/test_stackdriver_exporter.py
Outdated
Show resolved
Hide resolved
ext/opentelemetry-ext-stackdriver/src/opentelemetry/ext/stackdriver/utils.py
Outdated
Show resolved
Hide resolved
Hi, I tested it on my own GCP project, with examples/*.py (and cross-test client.py/server.py with server/client in opentelemetry-go/example/http-stackdriver) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks good, but I found some things that might need a fix :)
if not links: | ||
return None | ||
|
||
links = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this won't work.
An empty list is assigned to the links
function argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, single use function too.
"annotation": annotation_json, | ||
} | ||
) | ||
return {"timeEvent": logs} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit.
If I have a function called extract_events
I expect that it returns events, not logs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't define extract_events
at all, it is single use too.
return (result, truncated_byte_count) | ||
|
||
|
||
def extract_status(status: trace_api.Status): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These functions aren't supposed to be accessed "from the outside" and should be led by an underscore (see Jaeger implementation as an example).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even better, don't even define extract_status
. Its code is only used in one place, just move it there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even better, don't even define this function since it is single use. Just move its code where it is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it's necessary to remove all the single-use functions? Speaking because Jaeger and Zipkin exporters have some of them too (Example in Jaeger: _extract_refs_from_span
_extract_logs_from_span
)... And maybe they need a second look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it is necessary, but it is very convenient 🙂
They may be present somewhere else, but that does not mean we should have them here, of course. Removing them means less code, less stuff to document, but above all, it means that when you are reading code you don't have to stop, read code somewhere else (maybe doing some mental replacement of variable names that were passed as arguments) and then come back to the original point in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Thank you for taking the time to explain!
from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor | ||
|
||
trace.set_preferred_tracer_implementation(lambda T: Tracer()) | ||
tracer = trace.tracer() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# durations = 50 * 10 ** 6 | ||
# end_times = start_times + durations | ||
span_datas = [ | ||
Span( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is necessary to test multiple spans with different fields to make the tests more reliable. Example from Jaeger translation tests.
Half of the fields right now aren't tested (startTime, endTime, parent, links...).
class StackdriverSpanExporter(SpanExporter): | ||
"""Stackdriver span exporter for OpenTelemetry. | ||
|
||
Args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we are being strict on this, but PEP 257 says that the documentation for these arguments goes in the documentation for __init__
.
|
||
return SpanExportResult.SUCCESS | ||
|
||
def translate_to_stackdriver( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is better to avoid single call functions like this one because the reader has to look for the code here then return to this line.
parent_id = "{:016x}".format(span.parent.span_id) | ||
|
||
start_time = None | ||
if span.start_time: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If span.start_time
can be None
use if span.start_time is not None
.
|
||
logger = logging.getLogger(__name__) | ||
|
||
AGENT = "opentelemetry-python [{}]".format(__version__) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a single use global variable, better just move to where it is used.
"""Translate the spans to Stackdriver format. | ||
|
||
Args: | ||
spans: Tuple of spans to convert |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A list could also be used as the spans
argument.
return result | ||
|
||
|
||
def map_attributes(attribute_map): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Single use function here, too.
"""Convert the attributes to stackdriver attributes.""" | ||
if attribute_map is None: | ||
return attribute_map | ||
for (key, value) in attribute_map.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
attribute_map
is a dictionary, Instead of looking through all of its keys (O(N)), just access "attributeMap"
directly (O(1)).
return attribute_map | ||
|
||
|
||
def _format_attribute_value(value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Single use function too. Also, just something to consider:
>>> type(1).__name__
'int'
>>> type(False).__name__
'bool'
>>> type("string").__name__
'str'
>>> type(1.1).__name__
'float'
return result | ||
|
||
|
||
def check_str_length(str_to_check, limit=MAX_LENGTH): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just have 128 here instead of having a global variable named MAX_LENGTH
.
def test_export(self): | ||
trace_id = "6e0c63257de34c92bf9efcd03927272e" | ||
span_id = "95bb5edabd45950f" | ||
# start_times = 683647322 * 10 ** 9 # in ns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove commented code.
this should go into its own repo as it's a vendor exporter, no? |
Closed in favour of #698 |
* chore: update zipkin exporter README * chore: remove Prerequisites section
Implement #287
TODO:
Tested on: