-
Notifications
You must be signed in to change notification settings - Fork 59
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
feat(http-server): adds middleware instrumentation. #162
Conversation
} | ||
} | ||
|
||
public function error(Throwable $e, TraceContext $context, SpanCustomizer $span): void |
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.
fyi in brave we don't do this at the abstraction of http etc as often the throwable is generically parsable. Also in case it goes to another system, we don't set it at all (as it could be used in another field called exception for example)
ex we opt out like this:
if (error != null) return; // the call site used Span.error
And defer to zipkin handler to use an error parser which internally does this:
static String parse(Throwable error) {
if (error == null) throw new NullPointerException("error == null");
String message = error.getMessage();
if (message != null) return message;
if (error.getClass().isAnonymousClass()) { // avoids ""
return error.getClass().getSuperclass().getSimpleName();
}
return error.getClass().getSimpleName();
}
In the zipkin variant of the error reporter we generically convent errors using that parser:
In other words we used to put $span->tag(Tags\ERROR, $e->getMessage())
in different code, but then had to later centralize it, and then later defer it as when you use other tools like x-ray the tag shape is different.
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.
So if I understand correctly you don't treat error as something being part of an http tracing but instead you have an error parser that transform the error into something customized? I see that being feasible because Tags are objects in brave, in zipkin-php they are strings (I remember I wanted them to be stringable objects first but objects in PHP are not as cheap as in Java). To be honest, we are still in time to do such change (accept anything for tags and stringify them afterwards) because we are just introducing stronger typing and even more, if we don't do that change we won't be able to do it in the future but I am not entirely sure of the implications of such a change.
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 jist of the change is you store the Throwable and only parse it into a String tag at the last moment (converting to json for example). So our field used is MutableSpan.error
which is separate from MutableSpan.tags
.
here's the notes about that field which I think says all there is to be said (at least that I know :)) https://github.com/openzipkin/brave/blob/master/brave/src/main/java/brave/handler/MutableSpan.java#L49-L86
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 see. I did not know Span.error
was a thing in brave. Let me dig into it.
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.
} elseif ($this->requestSampler === null) { | ||
$span = $this->tracer->nextSpan($extractedContext); | ||
} else { | ||
$span = $this->tracer->nextSpanWithSampler( |
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.
In brave, we do this logic differently as it keeps the tracer api small which is helpful as sometimes there are many "if statements" inside, and keeping all inside nextSpan(extracted) helps sanity a little.
Span nextSpan(TraceContextOrSamplingFlags extracted, HttpServerRequest request) {
Boolean sampled = extracted.sampled();
// only recreate the context if the http sampler made a decision
if (sampled == null && (sampled = sampler.trySample(request)) != null) {
extracted = extracted.sampled(sampled.booleanValue());
}
return extracted.context() != null
? tracer.joinSpan(extracted.context())
: tracer.nextSpan(extracted);
}
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.
Check eb5bee4 where I added the nextSpanHandler
into the ServerTracing
.
$extractedContext | ||
); | ||
} | ||
$spanCustomizer = null; |
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.
instead of all the null checking, why not check if noop and early return $handler->handle($request);
?
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.
Totally. Let me change it.
* Parser includes the methods to obtain meaningful span information | ||
* out of HTTP request/response elements. | ||
*/ | ||
interface Parser |
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.
design note:
we used to have a design like this, but we don't any more. I already mentioned about the error handling in an earlier comment.
instead we have lambda compatible (single method) functions for the request and response side.
HttpRequestParser -> void parse(HttpRequest request, TraceContext context, SpanCustomizer span)
HttpResponseParser -> void parse(HttpResponse response, TraceContext context, SpanCustomizer span)
At least in Java, this allows easier composition. Ex you can rely on the default for span name etc and add a tag like so
.clientRequestParser((req, context, span) -> {
HttpClientRequestParser.DEFAULT.parse(req, context, span);
HttpTags.URL.tag(req, context, span); // add the url in addition to defaults
})
Most people need to change request not response tags anyway so it has little impact to split them, though the rationale may not be the same here.
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'd keep them together as IMHO it is easier to rationale about them together and also because I don't want to create too much classes around this.
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 made some notes and it appears to me that maybe due to the amount of deprecation we had in brave, it could have been less clear to follow vs rpc which is almost the same as our http handlers except it doesn't have all the deprecated things.
https://github.com/openzipkin/brave/blob/master/instrumentation/rpc
I made a bunch of comments I think maybe take a stab and think which are relevant here and let me know when you want another look.
@adriancole PTAL |
@adriancole I addressed most of your comments. I turned the requestSampler into the |
|
||
```php | ||
use Zipkin\Instrumentation\Http\Server\Middleware as ZipkinMiddleware; | ||
use Zipkin\Instrumentation\Http\Server\ServerTracing; |
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 see this is to avoid clash with normal Tracing. since RpcTracing may come next and that would have a client and server side.. and be configured possibly in the same file.. does it make more sense to use HttpTracing instead?
return $this->parser; | ||
} | ||
|
||
public function getNextSpanHandler(): callable |
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 this is only used once maybe it can live at its call site? having it here requires you to say why and how it is used
@@ -11,7 +11,7 @@ | |||
* ClientTracing includes all the elements needed to instrument a | |||
* HTTP client. | |||
*/ | |||
class ClientTracing | |||
class HttpClientTracing |
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.
Changed the name @adriancole
{ | ||
$extractedContext = ($this->extractor)($request); | ||
|
||
if ($extractedContext instanceof TraceContext) { |
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.
For now I am going to move this into a private method with a meaningful name.
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
final class Middleware implements MiddlewareInterface |
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 the PSR15 implementation using the server tracing.
* | ||
* @var callable(RequestInterface):?bool|null | ||
*/ | ||
private $requestSampler; | ||
|
||
public function __construct( | ||
Tracing $tracing, | ||
Parser $parser = null, | ||
Parser $parser, |
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.
Removing the null as it was intended for the HttpClientTracing to use a default one but since the default one requires a request type to be defined we require it as a parameter.
… to the default parsers.
@adriancole please take another look. I addressed most of your comments, I postponed the |
* into span name and tags. Implementors can use this as a base parser to reduce | ||
* boilerplate. | ||
*/ | ||
class DefaultParser implements Parser |
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 makes me curious. in brave, the parser is intended to be defined in a way that is not library specific. This allows it to be generated from static configuration that doesn't know anything about the types that would be used. What I'm seeing here is that DefaultParser and the data policy is being independently defined in different libraries.
I would recommend instead to keep parser language agnostic. similar to brave, make the caller of HttpTracing localize the Request interface. ex. Zipkin\Instrumentation\Http\HttpRequest
is the parameter of parser, and Zipkin\Instrumentation\Http\Server\Psr15\SomethingRequest
which can be package private, implements it.
that different libraries treat data differently is normal, but if the "parser of http requests" requires knowing which library is in use, we lose the ability to change tagging policy independent of library, ex from yaml.
ex notice here that "how" an http header is parsed is not something the user should care about.. it is just an implementation detail.
doing so allows you to configure ITs at global level, such as I think we also do in zipkin-js etc
https://github.com/openzipkin/brave/blob/master/instrumentation/http-tests/src/main/java/brave/test/http/ITHttpClient.java#L234-L237
that allows configuration or global config to change the data policy. Ex if X-Ray needs url you add code for that without caring knowing or linking to library is PSR etc... to do that.. make sense?
@adriancole I just added the Request/Response types and aligned the parsers. PTAL. |
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.
shaping up.. getting almost there!
class DefaultParser implements Parser | ||
{ | ||
public function spanName(RequestInterface $request): string | ||
public function spanName(Request $request): string |
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.
as span name can also be set (overridden) at response time, we don't expose a public function like this in brave. Just focus on request/response parsing vs suggesting where the span name is set. In the default impl we set the span name. Note: only in the default impl do we define the spanName method. it isn't in the generic interface anymore for this reason (that it doesn't belong to the request.. that name is just one example of something parsed) https://github.com/openzipkin/brave/blob/master/instrumentation/http/src/main/java/brave/http/HttpRequestParser.java#L79
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. Actually I was about to change this as using the API felt weird.
if ($response->getStatusCode() > 399) { | ||
$span->tag(Tags\ERROR, (string) $response->getStatusCode()); | ||
$statusCode = $response->getStatusCode(); | ||
$span->tag(Tags\HTTP_STATUS_CODE, (string) $statusCode); |
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.
status code is a little more nuanced than this:
maybe TODO if you don't want to address now, but we only tag if not in success range
https://github.com/openzipkin/brave/blob/master/instrumentation/http/src/main/java/brave/http/HttpResponseParser.java#L88
https://github.com/openzipkin/brave/blob/master/instrumentation/http/src/main/java/brave/http/HttpTags.java#L155-L162
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.
Good reminder. I wanted to introduce this before but I wasn't sure this applied for both client and server, by that time I wouldn't dive into the http instrumentation design so many doubts and I did not want to introduce a change I did not understand. Anyways good time to introduce it.
$span->tag(Tags\ERROR, (string) $response->getStatusCode()); | ||
$statusCode = $response->getStatusCode(); | ||
$span->tag(Tags\HTTP_STATUS_CODE, (string) $statusCode); | ||
if ($statusCode > 399) { |
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.
need also to not accidentally tag 100 as error
@@ -20,7 +19,7 @@ interface Parser | |||
* spanName returns an appropiate span name based on the request, | |||
* usually the HTTP method is good enough (e.g GET or POST). | |||
*/ | |||
public function spanName(RequestInterface $request): string; | |||
public function spanName(Request $request): string; |
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.
yeah see other comment about. as spanName can be set anytime, and is typically reset in the response based on http.route tag. might be better to not add this method to the base type
|
||
$tracing = TracingBuilder::create() | ||
->havingLocalServiceName('my_service') | ||
->build(); | ||
|
||
$httpClientTracing = new ClientTracing($tracing); | ||
$httpClientTracing = new HttpClientTracing($tracing, new DefaultParser); |
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.
drift? I presume default impl works now, right (ex don't have Psr specific parser)
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.
Yeah, totally. Also when using default parser we don't need to pass it. Honestly having to pass the DefaultParser
per library was akward.
* usually the HTTP method is enough (e.g GET or POST) but ideally | ||
* the http.route is desired (e.g. /user/{user_id}). | ||
*/ | ||
public function spanName(Request $request): string; |
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.
similar comment, that the client and server side have two different types that do the same things, means there are some opportunity for copy/paste problems.
->havingLocalServiceName('my_service') | ||
->build(); | ||
|
||
$httpClientTracing = new ServerTracing($tracing, new DefaultParser); |
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.
again user shouldn't have to instantiate a default parser anymore
* | ||
* @see Tags\HTTP_ROUTE | ||
*/ | ||
public function getRoute(): ?string; |
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.
route is also possible on some clients, not sure in PHP. It makes things easier to assume everything can see it and just handle if it is not visible vs 2 different parser hierarchies.
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 possible to pass router in PHP clients, I haven't seen any example. Since we know use abstract classes I think it is OK to not to add route yet and readd it later as it won't be a breaking change.
* | ||
* @see Tags\HTTP_ROUTE | ||
*/ | ||
public function getRoute(): ?string; |
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.
technically eventhough this is visible in the response, it is a part of request data. once we added response.request() we were able to keep this data in the more logical place https://github.com/openzipkin/brave/blob/master/instrumentation/http/src/main/java/brave/http/HttpRequest.java#L92-L106
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.
My concern about that is that if route is not there yet at request parsing, having the ability to parse later means we mutated the request. In any case I will check first what happens in laravel or symfony before settling on this.
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.
In the end I removed it as now we use abstract classes. I will readd it when the time comes.
@@ -30,7 +31,7 @@ private static function createTracing(): array | |||
$tracer = $tracing->getTracer(); | |||
|
|||
return [ | |||
new ClientTracing($tracing), | |||
new HttpClientTracing($tracing, new DefaultParser), |
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.
same comment
…s to make sure the introduction of new methods won't break implementations.
…o use the default one.
…methods that are likely to have a default value.
…nd hence method can be added later (when needed) without being a breaking change.
@adriancole I think I addressed all the feedback so please take another look and feel free to merge if you agree with all. |
This PR adds support for instrumentation in PSR middlewares.
Ping @adriancole