-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Instrumentation of Sanic Framework * initial instrumentation for sanic v21 * supporting sanic v20 * supporting v19 * uninstall uvloop, sanic optional dependency as it interferes with asynqp * Update .circleci/config.yml Co-authored-by: Manoj Pandey <[email protected]> * Update tests/conftest.py Co-authored-by: Manoj Pandey <[email protected]> * requested review changes and refactoring tests to class based Co-authored-by: Manoj Pandey <[email protected]>
- Loading branch information
1 parent
9872d14
commit 2e79688
Showing
11 changed files
with
618 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
""" | ||
Instrumentation for Sanic | ||
https://sanicframework.org/en/ | ||
""" | ||
try: | ||
import sanic | ||
import wrapt | ||
import opentracing | ||
from ..log import logger | ||
from ..singletons import async_tracer, agent | ||
from ..util.secrets import strip_secrets_from_query | ||
from ..util.traceutils import extract_custom_headers | ||
|
||
|
||
@wrapt.patch_function_wrapper('sanic.exceptions', 'SanicException.__init__') | ||
def exception_with_instana(wrapped, instance, args, kwargs): | ||
message = kwargs.get("message", args[0]) | ||
status_code = kwargs.get("status_code") | ||
span = async_tracer.active_span | ||
|
||
if all([span, status_code, message]) and (500 <= status_code <= 599): | ||
span.set_tag("http.error", message) | ||
try: | ||
wrapped(*args, **kwargs) | ||
except Exception as exc: | ||
span.log_exception(exc) | ||
else: | ||
wrapped(*args, **kwargs) | ||
|
||
|
||
def response_details(span, response): | ||
try: | ||
status_code = response.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) | ||
|
||
if response.headers is not None: | ||
async_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, response.headers) | ||
response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id | ||
except Exception: | ||
logger.debug("send_wrapper: ", exc_info=True) | ||
|
||
|
||
if hasattr(sanic.response.BaseHTTPResponse, "send"): | ||
@wrapt.patch_function_wrapper('sanic.response', 'BaseHTTPResponse.send') | ||
async def send_with_instana(wrapped, instance, args, kwargs): | ||
span = async_tracer.active_span | ||
if span is None: | ||
await wrapped(*args, **kwargs) | ||
else: | ||
response_details(span=span, response=instance) | ||
try: | ||
await wrapped(*args, **kwargs) | ||
except Exception as exc: | ||
span.log_exception(exc) | ||
raise | ||
else: | ||
@wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.write_response') | ||
def write_with_instana(wrapped, instance, args, kwargs): | ||
response = args[0] | ||
span = async_tracer.active_span | ||
if span is None: | ||
wrapped(*args, **kwargs) | ||
else: | ||
response_details(span=span, response=response) | ||
try: | ||
wrapped(*args, **kwargs) | ||
except Exception as exc: | ||
span.log_exception(exc) | ||
raise | ||
|
||
|
||
@wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.stream_response') | ||
async def stream_with_instana(wrapped, instance, args, kwargs): | ||
response = args[0] | ||
span = async_tracer.active_span | ||
if span is None: | ||
await wrapped(*args, **kwargs) | ||
else: | ||
response_details(span=span, response=response) | ||
try: | ||
await wrapped(*args, **kwargs) | ||
except Exception as exc: | ||
span.log_exception(exc) | ||
raise | ||
|
||
|
||
@wrapt.patch_function_wrapper('sanic.app', 'Sanic.handle_request') | ||
async def handle_request_with_instana(wrapped, instance, args, kwargs): | ||
|
||
try: | ||
request = args[0] | ||
try: # scheme attribute is calculated in the sanic handle_request method for v19, not yet present | ||
if "http" not in request.scheme: | ||
return await wrapped(*args, **kwargs) | ||
except AttributeError: | ||
pass | ||
headers = request.headers.copy() | ||
ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, headers) | ||
with async_tracer.start_active_span("asgi", child_of=ctx) as scope: | ||
scope.span.set_tag('span.kind', 'entry') | ||
scope.span.set_tag('http.path', request.path) | ||
scope.span.set_tag('http.method', request.method) | ||
scope.span.set_tag('http.host', request.host) | ||
scope.span.set_tag("http.url", request.url) | ||
|
||
query = request.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) | ||
scope.span.set_tag("http.params", scrubbed_params) | ||
|
||
if agent.options.extra_http_headers is not None: | ||
extract_custom_headers(scope, headers) | ||
await wrapped(*args, **kwargs) | ||
if hasattr(request, "uri_template"): | ||
scope.span.set_tag("http.path_tpl", request.uri_template) | ||
if hasattr(request, "ctx"): # ctx attribute added in the latest v19 versions | ||
request.ctx.iscope = scope | ||
except Exception as e: | ||
logger.debug("Sanic framework @ handle_request", exc_info=True) | ||
return await wrapped(*args, **kwargs) | ||
|
||
|
||
logger.debug("Instrumenting Sanic") | ||
|
||
except ImportError: | ||
pass | ||
except AttributeError: | ||
logger.debug("Not supported Sanic version") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
from ..singletons import agent | ||
from ..log import logger | ||
|
||
|
||
def extract_custom_headers(tracing_scope, headers): | ||
try: | ||
for custom_header in agent.options.extra_http_headers: | ||
# Headers are in the following format: b'x-header-1' | ||
for header_key, value in headers.items(): | ||
if header_key.lower() == custom_header.lower(): | ||
tracing_scope.span.set_tag("http.header.%s" % custom_header, value) | ||
except Exception as e: | ||
logger.debug("extract_custom_headers: ", exc_info=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
|
||
import uvicorn | ||
from ...helpers import testenv | ||
from instana.log import logger | ||
|
||
testenv["sanic_port"] = 1337 | ||
testenv["sanic_server"] = ("http://127.0.0.1:" + str(testenv["sanic_port"])) | ||
|
||
|
||
def launch_sanic(): | ||
from .server import app | ||
from instana.singletons import agent | ||
|
||
# Hack together a manual custom headers list; We'll use this in tests | ||
agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] | ||
|
||
uvicorn.run(app, host='127.0.0.1', port=testenv['sanic_port'], log_level="critical") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
|
||
from sanic.views import HTTPMethodView | ||
from sanic.response import text | ||
|
||
|
||
class NameView(HTTPMethodView): | ||
|
||
def get(self, request, name): | ||
return text("Hello {}".format(name)) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
from sanic import Sanic | ||
from sanic.exceptions import SanicException | ||
from .simpleview import SimpleView | ||
from .name import NameView | ||
from sanic.response import text | ||
import instana | ||
|
||
app = Sanic('test') | ||
|
||
@app.get("/foo/<foo_id:int>") | ||
async def uuid_handler(request, foo_id: int): | ||
return text("INT - {}".format(foo_id)) | ||
|
||
|
||
@app.route("/test_request_args") | ||
async def test_request_args(request): | ||
raise SanicException("Something went wrong.", status_code=500) | ||
|
||
|
||
@app.get("/tag/<tag>") | ||
async def tag_handler(request, tag): | ||
return text("Tag - {}".format(tag)) | ||
|
||
|
||
app.add_route(SimpleView.as_view(), "/") | ||
app.add_route(NameView.as_view(), "/<name>") | ||
|
||
|
||
if __name__ == '__main__': | ||
app.run(host="0.0.0.0", port=8000, debug=True, access_log=True) | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# (c) Copyright IBM Corp. 2021 | ||
# (c) Copyright Instana Inc. 2021 | ||
|
||
|
||
from sanic.views import HTTPMethodView | ||
from sanic.response import text | ||
|
||
class SimpleView(HTTPMethodView): | ||
|
||
def get(self, request): | ||
return text("I am get method") | ||
|
||
# You can also use async syntax | ||
async def post(self, request): | ||
return text("I am post method") | ||
|
||
def put(self, request): | ||
return text("I am put method") | ||
|
||
def patch(self, request): | ||
return text("I am patch method") | ||
|
||
def delete(self, request): | ||
return text("I am delete method") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.