Skip to content
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

Merged
merged 42 commits into from
Jul 14, 2020

Conversation

sgryphon
Copy link
Contributor

@sgryphon sgryphon commented Jun 3, 2020

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.

@apmmachine
Copy link
Contributor

apmmachine commented Jun 3, 2020

💚 Build Succeeded

Pipeline View Test View Changes Artifacts preview

Expand to view the summary

Build stats

  • Build Cause: [Pull request #85 updated]

  • Start Time: 2020-07-14T08:48:34.304+0000

  • Duration: 13 min 13 sec

Test stats 🧪

Test Results
Failed 0
Passed 50
Skipped 4
Total 54

Copy link
Member

@Mpdreamz Mpdreamz left a 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! 👍

@sgryphon
Copy link
Contributor Author

sgryphon commented Jun 3, 2020

Did you want to port the test project as part of this PR or do you intend to follow up?

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.

@Mpdreamz Mpdreamz added enhancement New feature or request v1.6.0 labels Jun 3, 2020
@sgryphon sgryphon requested a review from Mpdreamz June 4, 2020 07:17
@sgryphon
Copy link
Contributor Author

sgryphon commented Jun 4, 2020

The Jenkins build (Windows) had failed on the signing. I updated the project to use the shared properties which should fix this.

Copy link
Member

@Mpdreamz Mpdreamz left a 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!

@sgryphon sgryphon requested a review from Mpdreamz June 7, 2020 00:01
@sgryphon
Copy link
Contributor Author

sgryphon commented Jun 8, 2020

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)?

@sgryphon
Copy link
Contributor Author

sgryphon commented Jun 8, 2020

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.

Copy link
Contributor

@russcam russcam left a 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();
Copy link
Contributor

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));
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor

@gregkalapos gregkalapos left a 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, forcing Activity to use the new id format. Now, unfortunately in this case the Trace.Id will change (meaning we'll set Trace.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 on Activity, I think for that we'll need the APM Agent, I added a specific comment about this below.

var id = Guid.NewGuid().ToString();

var localClient = _lowLevelClient;
var response = localClient.Index<StringResponse>(index, id, PostData.Serializable(logEvent));
Copy link
Contributor

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 :)

Suggested change
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>())
Copy link
Contributor

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 🤔?

Suggested change
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>())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some as above.

Suggested change
foreach (var c in value.Cast<char>())
foreach (var c in value)

Comment on lines +30 to +31
private Regex _w3CFormat = new Regex(@"^[abcdef]{2}-[\dabcdef]{32}-([\dabcdef]{16})-[\dabcdef]{2}$",
RegexOptions.Compiled);
Copy link
Contributor

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.

Suggested change
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;
Copy link
Contributor

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 };
Copy link
Contributor

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;
Copy link
Contributor

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;

@Mpdreamz Mpdreamz force-pushed the feature/migrate-loggerprovider branch from e2d0f82 to 9a55fa7 Compare July 14, 2020 08:32
@Mpdreamz
Copy link
Member

@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.

@Mpdreamz Mpdreamz merged commit 764253b into elastic:master Jul 14, 2020
@Mpdreamz
Copy link
Member

Thanks again for your hard work on this @sgryphon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request v1.6.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants