begin static method
Starts tracing a request, or returns null when it must not be traced.
Null means the transport should carry the request out untouched: the Plugin has not started, the request is to the Collector Host (ADR-0006), or it matches EdotConfig.excludedUrls. There is no span, so nothing to end and no Trace Context to send — an untraced request has no span for a header to name.
client names the transport and reaches http.client, which is what tells two
integrations apart in a dashboard.
requestSize is the request body's size when the transport knows it. Absent
rather than zero when it does not: a stream of unknown length has no size to
report, and zero would read as an empty body.
Implementation
static EdotRequestTrace? begin({
required String method,
required String url,
required String client,
int? requestSize,
}) {
final rules = tracingRules;
if (rules == null) {
// Deliberately not buffered like other pre-start telemetry: without rules this
// cannot tell the Collector Host from any other host, and tracing the Agent's own
// export traffic is what ADR-0006 exists to prevent. Carrying on beats throwing —
// a transport is usually wired up during startup, and failing the app's first
// request over telemetry would be a poor trade.
//
// Sanitised even here, and even though no custom hook is configured yet: a
// debug log is somewhere the URL is recorded, and the query string it would
// otherwise carry is the reason the sanitiser exists.
edotLog('request before Edot.start; not traced: ${sanitizeUrl(url)}');
return null;
}
// Both exclusions match the URL as given, before sanitising — a pattern written
// against a query parameter has to be able to see it, and an excluded request
// produces no span, so nothing is recorded either way.
if (isCollectorHost(url, rules.collectorHost)) return null;
if (isExcluded(url, rules.excludedUrls)) return null;
// Everything downstream reads the sanitised URL, never the original — including
// the span name, which carries the host. A sanitiser that rewrites the authority
// has to be reflected there too, or the URL would be recorded in the one place
// nobody thought to sanitise.
final sanitizedUrl = sanitizeUrl(url, rules.urlSanitizer);
final attributes = <String, String>{
'http.method': method,
'http.url': sanitizedUrl,
'http.client': client,
};
// Each of these is absent rather than empty when the URL does not yield it. An
// attribute present with a meaningless value is worse than none.
final derived = <String, String?>{
'http.target': httpTarget(sanitizedUrl),
'http.scheme': httpScheme(sanitizedUrl),
'net.peer.name': peerName(sanitizedUrl),
};
derived.forEach((key, value) {
if (value != null) attributes[key] = value;
});
// Two spans, not one (ADR-0016). A request span with no parent is classified as
// a *transaction* at intake, and a transaction never carries a destination
// service — so Kibana has no exit span to draw a service map edge from, and no
// dependency to aggregate. The Request Transaction gives the request one to
// belong to, and matches the parent the iOS Agent manufactures for itself:
// same name, same kind, the same timestamps, and no attributes of its own.
final spans = Edot.tracer.startRequest(
spanNameFor(method, sanitizedUrl),
attributes: attributes,
);
final span = spans.request;
// Integers cannot travel with the creation attributes, which are strings only,
// so they follow immediately after.
final port = peerPort(sanitizedUrl);
if (port != null) span.setInt('net.peer.port', port);
if (requestSize != null) span.setInt('http.request_body.size', requestSize);
// Decided on the URL as given, for the same reason the exclusions are.
return EdotRequestTrace._(
spans,
shouldPropagate(url, rules.tracePropagationTargets),
);
}