Typed support for semantic conventions
tiffon opened this issue · 0 comments
Background
Currently, the semantic conventions for span tags and log fields are supported by way of constants (Go, Java, JavaScript).
Problem
- It's easier to ignore the conventions than it is to follow them
- Following the conventions by way of the constants makes for verbose code
- For a given standard key, the type of the value is not enforced in any way, e.g.
setTag('error', 99)
Proposal / goals
Provide a typed API which supports:
- The standard keys
- Value types
- Enum values (when applicable)
Existing interfaces should not need to be changed.
Third party developers should be able to follow the same approach to promote consistency within library or application code.
Thoughts on design
One approach is to define the key-value pairs on a separate object which is then used to either log the fields or to set the span tags:
tags.makeRPC()
.kind.isClient()
.peer.service('service-a')
.peer.hostname('le-hostname')
.peer.port(8080)
.setOnSpan(span);
This would set the following span tags:
span.kind: "client"
peer.service: "service-a"
peer.hostname: "le-hostname"
peer.port: 8080
The following standard tags would not be added to the span because they are not set in the code-snippet above:
peer.address
peer.ipv4
peer.ipv6
Enum values can be enforced via setter methods specific to each value. For instance, the .kind
property value would have .isClient()
and .isServer()
methods for RPC scenarios.
Similarly, to add a log record to a span:
try {
thingThatExplodes();
} catch (error) {
const errorLog = logs.makeError()
.message(error.message)
.stack(error.stack)
.kind(error.constructor.name);
span.log(errorLog);
}
This would add a log record with the following fields:
event: "error"
error.kind: "..."
– determined by the constructor oferror
message: "..."
– determined byerror.message
stack: "..."
– determined byerror.stack
An alternate API to add a log record:
try {
thingThatExplodes();
} catch (error) {
logs.makeError()
.message(error.message)
.stack(error.stack)
.kind(error.constructor.name)
.logToSpan(span);
}
The approaches described above are scenario based and make use of a specific subset of the standard span tags and log fields. The same approach can be used to provide general typed support for the standard tags and fields:
tags.make()
.component('omg-layer')
.span.kind.isClient()
.errored()
.http.status_code(404)
.setOnSpan(span);
This would set the following span tags:
component: "omg-layer"
span.kind: "client"
error: true
http.status_code: 404
One benefit of the scenario approach is that certain expectations can be enforced, as determined by the spec: Modeling special circumstances. For instance, the following omits the span.kind
tag and therefore can result in a warning or error:
// this would warn or error
tags.makeRPC()
.peer.service('service-a')
.peer.hostname('le-hostname')
.peer.port(8080)
.setOnSpan(span);
An alternative style for the API:
const rpcTags = tags.makeRPC();
rpcTags.span.kind = 'client';
rpcTags.peer.service = 'the-other-service';
rpcTags.setOnSpan(span);
This style is simpler and perhaps more approachable, but is more verbose and it would be more difficult to enforce required keys at compile time (to my understanding).
Lastly
The initial thinking is that the main semantic conventions can live in the core API and extensions can live in contrib.