Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a763dee
Load GRPC server on-demand only
Oct 21, 2020
4e6e143
Tests: Support launching threads with args
Oct 21, 2020
204810b
Python 2 compatibility
Oct 22, 2020
ac5819c
Avoid using lambda for test suite: multiprocessing & pickle
Oct 22, 2020
7af1ac1
New test suite dependencies
Oct 22, 2020
edacc37
Add support & tests for byte based context keys
Oct 26, 2020
a13a212
Set test env vars
Oct 26, 2020
5cf8cc0
Reorg middleware exports
Oct 26, 2020
943edb3
When in test, we can always send
Oct 26, 2020
a2685a5
Check for empty first
Oct 26, 2020
e857e9b
Add support for byte based context headers
Oct 26, 2020
eeb93df
New ASGI middleware for FastAPI & Starlette
Oct 27, 2020
37f5aec
Add ASGI as registered span
Oct 27, 2020
5f0c619
Tests: FastAPI background server & tests
Oct 27, 2020
898dbf5
Check potential byte based value
Oct 27, 2020
592d37f
Better custom header capture
Oct 27, 2020
a5533a3
Set custom header options before launching background process
Oct 27, 2020
cc9c73f
Vanilla, synthetic and custom header capture tests
Oct 27, 2020
9aac94f
Tests: Secret scrubbing
Oct 27, 2020
2633f29
Path Templates support & tests
Oct 27, 2020
5936ae2
FastAPI Test server cleanup
Oct 28, 2020
2ef0235
Test path templates always
Oct 28, 2020
c38728e
Starlette tests: background server and tests
Oct 28, 2020
7b25f08
Version limiters for CircleCI
Oct 28, 2020
b5a3da3
Fix version string
Oct 28, 2020
19dc176
Version limit uvicorn
Oct 28, 2020
df51bec
Fix Python 2 support
Oct 28, 2020
e0dc9b1
Dont use __getitem__: Final answer
Oct 28, 2020
25d1f1c
Update opentracing version
Oct 29, 2020
d2fedea
Update basictracer version
Oct 29, 2020
8a9bc83
When in test, use a multiprocess queue
Oct 29, 2020
6a0f601
Code comments
Oct 29, 2020
6f54426
Simplify multiprocess launching
Oct 29, 2020
3c03752
In tests, pause for spans to land
Oct 29, 2020
c8be071
Break up aiohttp tests
Oct 29, 2020
881f39d
More robust extraction
Oct 29, 2020
71714e7
Assure package loaded
Oct 29, 2020
6b1bd0b
Remove redundant sleeps
Oct 29, 2020
3fc84d0
Pause to let spans settle
Oct 29, 2020
b54fa66
Starlette requires aiofiles
Oct 29, 2020
431919b
Better conversion for Tornado headers class
Oct 29, 2020
e771061
Unify, cleanup & fix context propagators
Nov 4, 2020
326ac45
Fix binary propagator tests
Nov 4, 2020
38d54a7
Safeties, maturities and updated tests
Nov 4, 2020
6ce0218
Cleanup: remove debug & pydoc
Nov 5, 2020
885fcd9
Cleanup; Remove debug logs
Nov 5, 2020
aae259a
Maturity Refactoring
Nov 5, 2020
41ef888
Breakout gunicorn detection
Nov 9, 2020
a60e043
Make log package independent to avoid circular import issues
Nov 9, 2020
5c861fd
Reload gunicorn on AutoTrace
Nov 9, 2020
8dd3607
New Test Helper: launch_traced_request
Nov 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions instana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def boot_agent():
# Import & initialize instrumentation
from .instrumentation.aws import lambda_inst

if sys.version_info >= (3, 6, 0):
from .instrumentation import fastapi_inst
from .instrumentation import starlette_inst

if sys.version_info >= (3, 5, 3):
from .instrumentation import asyncio
from .instrumentation.aiohttp import client
Expand Down
3 changes: 3 additions & 0 deletions instana/agent/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ def can_send(self):
Are we in a state where we can send data?
@return: Boolean
"""
if "INSTANA_TEST" in os.environ:
return True

# Watch for pid change (fork)
self.last_fork_check = datetime.now()
current_pid = os.getpid()
Expand Down
2 changes: 1 addition & 1 deletion instana/collector/aws_fargate.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Snapshot & metrics collection for AWS Fargate
AWS Fargate Collector: Manages the periodic collection of metrics & snapshot data
"""
import os
import json
Expand Down
2 changes: 1 addition & 1 deletion instana/collector/aws_lambda.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Snapshot & metrics collection for AWS Lambda
AWS Lambda Collector: Manages the periodic collection of metrics & snapshot data
"""
from ..log import logger
from .base import BaseCollector
Expand Down
10 changes: 9 additions & 1 deletion instana/collector/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ def __init__(self, agent):
self.THREAD_NAME = "Instana Collector"

# The Queue where we store finished spans before they are sent
self.span_queue = queue.Queue()
if env_is_test:
# Override span queue with a multiprocessing version
# The test suite runs background applications - some in background threads,
# others in background processes. This multiprocess queue allows us to collect
# up spans from all sources.
import multiprocessing
self.span_queue = multiprocessing.Queue()
else:
self.span_queue = queue.Queue()

# The Queue where we store finished profiles before they are sent
self.profile_queue = queue.Queue()
Expand Down
3 changes: 1 addition & 2 deletions instana/collector/host.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""
Snapshot & metrics collection for AWS Fargate
Host Collector: Manages the periodic collection of metrics & snapshot data
"""
from time import time
from ..log import logger
from .base import BaseCollector
from ..util import DictionaryOfStan
from ..singletons import env_is_test
from .helpers.runtime import RuntimeHelper


Expand Down
100 changes: 100 additions & 0 deletions instana/instrumentation/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Instana ASGI Middleware
"""
import opentracing

from ..log import logger
from ..singletons import async_tracer, agent
from ..util import strip_secrets_from_query

class InstanaASGIMiddleware:
"""
Instana ASGI Middleware
"""
def __init__(self, app):
self.app = app

def _extract_custom_headers(self, span, headers):
try:
for custom_header in agent.options.extra_http_headers:
# Headers are in the following format: b'x-header-1'
for header_pair in headers:
if header_pair[0].decode('utf-8').lower() == custom_header.lower():
span.set_tag("http.%s" % custom_header, header_pair[1].decode('utf-8'))
except Exception:
logger.debug("extract_custom_headers: ", exc_info=True)

def _collect_kvs(self, scope, span):
try:
span.set_tag('http.path', scope.get('path'))
span.set_tag('http.method', scope.get('method'))

server = scope.get('server')
if isinstance(server, tuple):
span.set_tag('http.host', server[0])

query = scope.get('query_string')
if isinstance(query, (str, bytes)) and len(query):
if isinstance(query, bytes):
query = query.decode('utf-8')
scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, agent.options.secrets_list)
span.set_tag("http.params", scrubbed_params)

app = scope.get('app')
if app is not None and hasattr(app, 'routes'):
# Attempt to detect the Starlette routes registered.
# If Starlette isn't present, we harmlessly dump out.
from starlette.routing import Match
for route in scope['app'].routes:
if route.matches(scope)[0] == Match.FULL:
span.set_tag("http.path_tpl", route.path)
except Exception:
logger.debug("ASGI collect_kvs: ", exc_info=True)


async def __call__(self, scope, receive, send):
request_context = None

if scope["type"] not in ("http", "websocket"):
await self.app(scope, receive, send)
return

request_headers = scope.get('headers')
if isinstance(request_headers, list):
request_context = async_tracer.extract(opentracing.Format.BINARY, request_headers)

async def send_wrapper(response):
span = async_tracer.active_span
if span is None:
await send(response)
else:
if response['type'] == 'http.response.start':
try:
status_code = response.get('status')
if status_code is not None:
if 500 <= int(status_code) <= 511:
span.mark_as_errored()
span.set_tag('http.status_code', status_code)

headers = response.get('headers')
if headers is not None:
async_tracer.inject(span.context, opentracing.Format.BINARY, headers)
except Exception:
logger.debug("send_wrapper: ", exc_info=True)

try:
await send(response)
except Exception as exc:
span.log_exception(exc)
raise

with async_tracer.start_active_span("asgi", child_of=request_context) as tracing_scope:
self._collect_kvs(scope, tracing_scope.span)
if 'headers' in scope and agent.options.extra_http_headers is not None:
self._extract_custom_headers(tracing_scope.span, scope['headers'])

try:
await self.app(scope, receive, send_wrapper)
except Exception as exc:
tracing_scope.span.log_exception(exc)
raise exc
33 changes: 33 additions & 0 deletions instana/instrumentation/fastapi_inst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Instrumentation for FastAPI
https://fastapi.tiangolo.com/
"""
try:
import fastapi
import wrapt
import signal
import os

from ..log import logger
from ..util import running_in_gunicorn
from .asgi import InstanaASGIMiddleware
from starlette.middleware import Middleware

@wrapt.patch_function_wrapper('fastapi.applications', 'FastAPI.__init__')
def init_with_instana(wrapped, instance, args, kwargs):
middleware = kwargs.get('middleware')
if middleware is None:
kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)]
elif isinstance(middleware, list):
middleware.append(Middleware(InstanaASGIMiddleware))

return wrapped(*args, **kwargs)

logger.debug("Instrumenting FastAPI")

# Reload GUnicorn when we are instrumenting an already running application
if "INSTANA_MAGIC" in os.environ and running_in_gunicorn():
os.kill(os.getpid(), signal.SIGHUP)

except ImportError:
pass
14 changes: 7 additions & 7 deletions instana/instrumentation/pyramid/tweens.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@

class InstanaTweenFactory(object):
"""A factory that provides Instana instrumentation tween for Pyramid apps"""

def __init__(self, handler, registry):
self.handler = handler

def __call__(self, request):
ctx = tracer.extract(ot.Format.HTTP_HEADERS, request.headers)
ctx = tracer.extract(ot.Format.HTTP_HEADERS, dict(request.headers))
scope = tracer.start_active_span('http', child_of=ctx)

scope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER)
Expand All @@ -42,7 +42,7 @@ def __call__(self, request):
response = None
try:
response = self.handler(request)

tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, response.headers)
response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id
except HTTPException as e:
Expand All @@ -53,21 +53,21 @@ def __call__(self, request):

# we need to explicitly populate the `message` tag with an error here
# so that it's picked up from an SDK span
scope.span.set_tag("message", str(e))
scope.span.set_tag("message", str(e))
scope.span.log_exception(e)

logger.debug("Pyramid Instana tween", exc_info=True)
finally:
if response:
scope.span.set_tag("http.status", response.status_int)

if 500 <= response.status_int <= 511:
if response.exception is not None:
message = str(response.exception)
scope.span.log_exception(response.exception)
else:
message = response.status

scope.span.set_tag("message", message)
scope.span.assure_errored()

Expand Down
24 changes: 24 additions & 0 deletions instana/instrumentation/starlette_inst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Instrumentation for Starlette
https://www.starlette.io/
"""
try:
import starlette
import wrapt
from ..log import logger
from .asgi import InstanaASGIMiddleware
from starlette.middleware import Middleware

@wrapt.patch_function_wrapper('starlette.applications', 'Starlette.__init__')
def init_with_instana(wrapped, instance, args, kwargs):
middleware = kwargs.get('middleware')
if middleware is None:
kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)]
elif isinstance(middleware, list):
middleware.append(Middleware(InstanaASGIMiddleware))

return wrapped(*args, **kwargs)

logger.debug("Instrumenting Starlette")
except ImportError:
pass
4 changes: 3 additions & 1 deletion instana/instrumentation/tornado/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
def execute_with_instana(wrapped, instance, argv, kwargs):
try:
with tracer_stack_context():
ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, instance.request.headers)
ctx = None
if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__:
ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, instance.request.headers.__dict__['_dict'])
scope = tornado_tracer.start_active_span('tornado-server', child_of=ctx)

# Query param scrubbing
Expand Down
55 changes: 55 additions & 0 deletions instana/instrumentation/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Instana WSGI Middleware
"""
import opentracing as ot
import opentracing.ext.tags as tags

from ..singletons import agent, tracer
from ..util import strip_secrets_from_query


class InstanaWSGIMiddleware(object):
""" Instana WSGI middleware """

def __init__(self, app):
self.app = app

def __call__(self, environ, start_response):
env = environ

def new_start_response(status, headers, exc_info=None):
"""Modified start response with additional headers."""
tracer.inject(self.scope.span.context, ot.Format.HTTP_HEADERS, headers)
headers.append(('Server-Timing', "intid;desc=%s" % self.scope.span.context.trace_id))

res = start_response(status, headers, exc_info)

sc = status.split(' ')[0]
if 500 <= int(sc) <= 511:
self.scope.span.mark_as_errored()

self.scope.span.set_tag(tags.HTTP_STATUS_CODE, sc)
self.scope.close()
return res

ctx = tracer.extract(ot.Format.HTTP_HEADERS, env)
self.scope = tracer.start_active_span("wsgi", child_of=ctx)

if agent.options.extra_http_headers is not None:
for custom_header in agent.options.extra_http_headers:
# Headers are available in this format: HTTP_X_CAPTURE_THIS
wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_')
if wsgi_header in env:
self.scope.span.set_tag("http.%s" % custom_header, env[wsgi_header])

if 'PATH_INFO' in env:
self.scope.span.set_tag('http.path', env['PATH_INFO'])
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list)
self.scope.span.set_tag("http.params", scrubbed_params)
if 'REQUEST_METHOD' in env:
self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD'])
if 'HTTP_HOST' in env:
self.scope.span.set_tag("http.host", env['HTTP_HOST'])

return self.app(environ, new_start_response)
Loading