-
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
Stand alone logger provider for Microsoft.Extensions.Logging that uses ECS to log directly to Elasticsearch #85
Stand alone logger provider for Microsoft.Extensions.Logging that uses ECS to log directly to Elasticsearch #85
Conversation
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.
Some initial surface nitpicks, can you also reformat the code. Our .editorconfig should dictate tabs if you use visual studio/rider/omnisharp.
Did you want to port the test project as part of this PR or do you intend to follow up?
Again thank you for this! 👍
src/Elastic.CommonSchema.LoggerProvider/Elastic.CommonSchema.LoggerProvider.csproj
Outdated
Show resolved
Hide resolved
src/Elastic.CommonSchema.LoggerProvider/.config/dotnet-tools.json
Outdated
Show resolved
Hide resolved
src/Elastic.CommonSchema.LoggerProvider/Elastic.CommonSchema.LoggerProvider.csproj
Outdated
Show resolved
Hide resolved
I don't have a unit test project (I did have for my Framework System.Diagnostics trace listener libraries, but haven't ported them across to Core Extensions.Logging ... it is on my todo list) I'll do a separate PR for the example / how to use. |
The Jenkins build (Windows) had failed on the signing. I updated the project to use the shared properties which should fix 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.
Thanks for the updates, CI is happy. I added a few more notes
@gregkalapos @russcam I would love another pass over by atleast one of you!
src/Elastic.CommonSchema.LoggerProvider/ElasticsearchDataProcessor.cs
Outdated
Show resolved
Hide resolved
src/Elastic.CommonSchema.LoggerProvider/ElasticsearchDataProcessor.cs
Outdated
Show resolved
Hide resolved
This is approved, but I don't have permission to merge. From the code review, the only open question is whether to change the namespace, e.g. Elastic.Extensions.Logging or Elasticsearch.Extensions.Logging (if you want Elasticsearch in there)? |
I decided to change the namespace to "Elasticsearch.Extensions.Logging", which has the Elasticsearch name in there (where it is writing to directly) and reflects it is for Extensions.Logging. |
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've had a first pass through and left some initial comments
|
||
var index = string.Format(_options.Index, indexTime); | ||
|
||
var id = Guid.NewGuid().ToString(); |
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 the id for a document is auto-generated, it'd be better to not send an id and rely on Elasticsearch to generate one. This is faster in Elasticsearch because it does not need to check if a document already exists with a given id.
var id = Guid.NewGuid().ToString(); | ||
|
||
var localClient = _lowLevelClient; | ||
var response = localClient.Index<StringResponse>(index, id, PostData.Serializable(logEvent)); |
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.
Should a failed API call be handled or logged?
{ | ||
try | ||
{ | ||
foreach (var logEvent in _messageQueue.GetConsumingEnumerable()) PostEvent(logEvent); |
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.
Using the bulk API to send a batch of events in one request would be preferable to sending individual log events.
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.
Awesome work @sgryphon - thank you for doing this!
I went through the whole thing, I added some things I noticed, overall I feel this is very good!
One thing I'd like to rant about here is the Tracing
part of ecs (I work on the Elastic APM Agent, so that's close to me :) )
To fill those fields we typically have another package which depends on the .NET APM Agent, here you used Activity
. I feel the Elasticsearch.Extensions.Logging
package should not depend on the APM Agent, so using Activity
is totally reasonable. Plus in case there is no APM Agent in the app, then indeed we should use Activity
.
But a couple of things:
- We have a feature in Kibana which lets you jump from APM Traces to Logs and vice versa, and that feature works based on
Trace.Id
. - The Elastic APM Agent works with W3C TraceContext based ids -
Activity
has 2 id formats, the hierarchical (that's still the default) ones and W3C (the one which the Elastic APM Agent also uses) - If the APM Agent is present in an App, it'll always check if there is a W3C TraceId already created and for that it uses
Activity.Currernt
,- if yes, it'll reuse it, so that's good, all log and APM trace correlation will work perfectly
- if not (because the old hierarchical format is used), then it'll create one and it'll also create an
Activity
with a W3C id format, forcingActivity
to use the new id format. Now, unfortunately in this case theTrace.Id
will change (meaning we'll setTrace.Id
based on the hierarchical id until the APM transaction is started and then after the APM transaction starts it'll be a new W3C based id). This is a bit unfortunate, but in the readme here we already suggest using W3C format, so I'd say that's just something we will live with.
- The
Transaction.Id
is not used at the moment by any Kibana or APM feature for log correlation. I feel we won't be able to populate that based onActivity
, I think for that we'll need the APM Agent, I added a specific comment about this below.
src/Elasticsearch.Extensions.Logging/Elasticsearch.Extensions.Logging.csproj
Show resolved
Hide resolved
var id = Guid.NewGuid().ToString(); | ||
|
||
var localClient = _lowLevelClient; | ||
var response = localClient.Index<StringResponse>(index, id, PostData.Serializable(logEvent)); |
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.
nit: var response
unused, unless we handle it as @russcam suggests :)
var response = localClient.Index<StringResponse>(index, id, PostData.Serializable(logEvent)); | |
localClient.Index<StringResponse>(index, id, PostData.Serializable(logEvent)); |
|
||
private void WriteName(StringBuilder stringBuilder, string name) | ||
{ | ||
foreach (var c in name.Cast<char>()) |
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.
nit: Is this .Cast<char>()
needed 🤔?
foreach (var c in name.Cast<char>()) | |
foreach (var c in name) |
|
||
private void WriteValue(StringBuilder stringBuilder, string value) | ||
{ | ||
foreach (var c in value.Cast<char>()) |
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.
Some as above.
foreach (var c in value.Cast<char>()) | |
foreach (var c in value) |
private Regex _w3CFormat = new Regex(@"^[abcdef]{2}-[\dabcdef]{32}-([\dabcdef]{16})-[\dabcdef]{2}$", | ||
RegexOptions.Compiled); |
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.
nit: seems to be unused.
private Regex _w3CFormat = new Regex(@"^[abcdef]{2}-[\dabcdef]{32}-([\dabcdef]{16})-[\dabcdef]{2}$", | |
RegexOptions.Compiled); |
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Text.RegularExpressions; |
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 case private Regex _w3CFormat
is removed this can also be removed.
// Unique identifier of the transaction. | ||
// A transaction is the highest level of work measured within a service, such as a request to a server. | ||
var spanId = ExtractW3cSpanIdFromActivityId(activity.Id); | ||
logEvent.Transaction = new Transaction() { Id = spanId ?? activity.Id }; |
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 think setting Transaction.Id
to Activity.Current.Id
or to SpanId
in case it's in W3C format is not correct. As the definition of transaction
in ecs says (which is in the comment above) "transaction is the highest level of work measured within a service". The thinking behind this is that if you filter based on transaction.id
you'll see all logs for the given transaction e.g. for a given incoming HTTP request (from the point when it hits the request pipeline until the response is sent back) in a single service. The scope of SpanId or Activity.Id
is smaller - it'd be something like an outgoing HTTP request or a database call - but each activity
will have its own id, so if we use those, then the filtering based on Transaction.Id
will be very different, you'll only see logs for a given span (or activity), but not for the whole transaction.
In Elastic APM we have the definition of a transaction which is not the same as a span - Activity
fits more the definition of the span.
We have tracing integration with Serilog and NLog with the Elastic APM Agent (meaning we fill the 2 ecs tracing fields (Trace.Id
and Transaction.Id
) based on ids generated by the APM Agent) and in that case we set Transaction.Id
to the real transaction id - which is the same for all spans within a single service.
Overall I'm afraid it'd be impossible to get a transaction.id just based on Activity
here and I feel it'd be best to not fill Transaction.Id
here. We could reiterate on this later, but for now I think it'd be better to just not set it to something which would be technically incorrect. So my suggestion is that we should remove setting the logEvent.Transaction
part.
Happy to discuss this further, let me know if someone has different opinion.
|
||
private string? ExtractW3cSpanIdFromActivityId(string activityId) | ||
{ | ||
if (activityId.Length != 2 + 32 + 16 + 2 + 3) return 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.
Probably an edge case, but I think a non-W3C activityId can also be 2 + 32 + 16 + 2 + 3
long.
An idea: if it's not W3C then it's probably the hierarchical id and that always starts with |
, so a check like this would also catch that:
if (activityId[0] == '|' || activityId.Length != 2 + 32 + 16 + 2 + 3) return null;
… so the project name is set correctly
….cs, and 8 more files...
… and 11 more files...
e2d0f82
to
9a55fa7
Compare
@russcam @gregkalapos discussed this over zoom and decided to merge this as is and follow up with smaller PR's to get this ready for release. For now the new project is marked as non pack-able to prevent a release. I will open a new ticket to keep track of the work needed. |
Thanks again for your hard work on this @sgryphon! |
Component donated from https://github.com/sgryphon/essential-logging
Re-licensed as Apache 2 (contributer agreement signed).
Once merged, I will deprecate from the other repository and refer here.
Also once merged I can update the pull request with the usage example to refer to the project directly.