-
Notifications
You must be signed in to change notification settings - Fork 165
[AppSec] Api to allow user data to be associated with the local root span #2546
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
Changes from all commits
eb52272
e8001e4
3a10e66
87a4526
88ee788
9d11fc6
c916984
22ea140
b1b9ed4
49f4947
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,6 +277,35 @@ Note: Call-target instrumentation does not support instrumenting custom implemen | |
|
|
||
| The `integrations.json` file is no longer required for instrumentation. You can remove references to this file, for example by deleting the `DD_INTEGRATIONS` environment variable. | ||
|
|
||
| ### User Identification | ||
|
|
||
| The tracer provides a convenient method to link an actor to a trace. You have to pass a `UserDetails` object with at least its Id property set to a non-null value. | ||
|
|
||
| To correlate users to web requests, you would need to use the following code snippet within each web request, after user authorization has been performed: | ||
|
|
||
| ```csharp | ||
| using Datadog.Trace; | ||
|
|
||
| // ... | ||
|
|
||
| var userDetails = new UserDetails() | ||
| { | ||
| // the systems internal identifier for the users | ||
| Id = "d41452f2-483d-4082-8728-171a3570e930", | ||
| // the email address of the user | ||
| Email = "[email protected]", | ||
| // the user's name, as displayed by the system | ||
| Name = "Jane Doh", | ||
| // the user's session id | ||
| SessionId = "d0632156-132b-4baa-95b2-a492c5f9cb16", | ||
| // the role the user is making the request under | ||
| Role = "standard", | ||
| }; | ||
| Tracer.Instance.ActiveScope?.Span.SetUser(userDetails); | ||
|
|
||
| ``` | ||
|
|
||
| ## Get in touch | ||
|
|
||
| If you have questions, feedback, or feature requests, reach our [support](https://docs.datadoghq.com/help). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // <copyright file="SpanExtensions.cs" company="Datadog"> | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
| // </copyright> | ||
|
|
||
| using Datadog.Trace.Util; | ||
|
|
||
| namespace Datadog.Trace | ||
| { | ||
| /// <summary> | ||
| /// Extension methods for the <see cref="ISpan"/> interface | ||
| /// </summary> | ||
| public static class SpanExtensions | ||
| { | ||
| /// <summary> | ||
| /// Sets the details of the user on the local root span | ||
| /// </summary> | ||
| /// <param name="span">The span to be tagged</param> | ||
| /// <param name="userDetails">The details of the current logged on user</param> | ||
| public static void SetUser(this ISpan span, UserDetails userDetails) | ||
| { | ||
| if (span is null) | ||
| { | ||
| ThrowHelper.ThrowArgumentNullException(nameof(span)); | ||
| } | ||
|
|
||
| if (userDetails is null) | ||
| { | ||
| ThrowHelper.ThrowArgumentNullException(nameof(userDetails)); | ||
| } | ||
|
|
||
| if (userDetails.Id is null) | ||
| { | ||
| ThrowHelper.ThrowArgumentException(nameof(userDetails) + ".Id must be set to a value other than null", nameof(userDetails)); | ||
| } | ||
|
|
||
| var localRootSpan = span; | ||
| if (span is Span spanClass) | ||
| { | ||
| localRootSpan = spanClass.Context.TraceContext?.RootSpan ?? span; | ||
| } | ||
|
|
||
| if (userDetails.Id is not null) | ||
| { | ||
| localRootSpan.SetTag(Tags.User.Id, userDetails.Id); | ||
| } | ||
|
|
||
| if (userDetails.Email is not null) | ||
| { | ||
| localRootSpan.SetTag(Tags.User.Email, userDetails.Email); | ||
| } | ||
|
|
||
| if (userDetails.Name is not null) | ||
| { | ||
| localRootSpan.SetTag(Tags.User.Name, userDetails.Name); | ||
| } | ||
|
|
||
| if (userDetails.SessionId is not null) | ||
| { | ||
| localRootSpan.SetTag(Tags.User.SessionId, userDetails.SessionId); | ||
| } | ||
|
|
||
| if (userDetails.Role is not null) | ||
| { | ||
| localRootSpan.SetTag(Tags.User.Role, userDetails.Role); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // <copyright file="UserDetails.cs" company="Datadog"> | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
| // </copyright> | ||
|
|
||
| namespace Datadog.Trace | ||
| { | ||
| /// <summary> | ||
| /// A data container class for the users details | ||
| /// </summary> | ||
| public class UserDetails | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If users are expected to create If users decide to cache the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the struct part, but I don't think we should use From the docs:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the idea is to use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mentioned
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The trouble with Struct is still an improvement, we just have to accept the copy I think 🤷♂️
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I forgot this is how |
||
| { | ||
| /// <summary> | ||
| /// Gets or sets the user's email address | ||
| /// </summary> | ||
| public string Email { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the user's name as displayed in the UI | ||
| /// </summary> | ||
| public string Name { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the unique identifier assoicated with the users | ||
| /// </summary> | ||
| public string Id { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the user's session unique identifier | ||
| /// </summary> | ||
| public string SessionId { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the role associated with the user | ||
| /// </summary> | ||
| public string Role { get; set; } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| // </copyright> | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Net; | ||
| using System.Threading.Tasks; | ||
|
|
@@ -524,5 +525,175 @@ public async Task ForceFlush() | |
|
|
||
| agent.Verify(a => a.FlushTracesAsync(), Times.Once); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetUserOnRootSpanDirectly() | ||
|
robertpi marked this conversation as resolved.
|
||
| { | ||
| var scopeManager = new AsyncLocalScopeManager(); | ||
|
|
||
| var settings = new TracerSettings | ||
| { | ||
| StartupDiagnosticLogEnabled = false | ||
| }; | ||
| var tracer = new Tracer(settings, Mock.Of<IAgentWriter>(), Mock.Of<ISampler>(), scopeManager, Mock.Of<IDogStatsd>()); | ||
|
|
||
| var rootTestScope = tracer.StartActive("test.trace"); | ||
| var childTestScope = tracer.StartActive("test.trace.child"); | ||
| childTestScope.Dispose(); | ||
|
|
||
| var email = "[email protected]"; | ||
| var name = "Jane Doh"; | ||
| var id = Guid.NewGuid().ToString(); | ||
| var sessionId = Guid.NewGuid().ToString(); | ||
| var role = "admin"; | ||
|
|
||
| var userDetails = new UserDetails() | ||
| { | ||
| Email = email, | ||
| Name = name, | ||
| Id = id, | ||
| SessionId = sessionId, | ||
| Role = role, | ||
| }; | ||
| tracer.ActiveScope?.Span.SetUser(userDetails); | ||
|
|
||
| Assert.Equal(email, rootTestScope.Span.GetTag(Tags.User.Email)); | ||
| Assert.Equal(name, rootTestScope.Span.GetTag(Tags.User.Name)); | ||
| Assert.Equal(id, rootTestScope.Span.GetTag(Tags.User.Id)); | ||
| Assert.Equal(sessionId, rootTestScope.Span.GetTag(Tags.User.SessionId)); | ||
| Assert.Equal(role, rootTestScope.Span.GetTag(Tags.User.Role)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetUserOnChildChildSpan_ShouldAttachToRoot() | ||
| { | ||
| var scopeManager = new AsyncLocalScopeManager(); | ||
|
|
||
| var settings = new TracerSettings | ||
| { | ||
| StartupDiagnosticLogEnabled = false | ||
| }; | ||
| var tracer = new Tracer(settings, Mock.Of<IAgentWriter>(), Mock.Of<ISampler>(), scopeManager, Mock.Of<IDogStatsd>()); | ||
|
|
||
| var rootTestScope = tracer.StartActive("test.trace"); | ||
| var childTestScope = tracer.StartActive("test.trace.child"); | ||
|
|
||
| var email = "[email protected]"; | ||
| var name = "Jane Doh"; | ||
| var id = Guid.NewGuid().ToString(); | ||
| var sessionId = Guid.NewGuid().ToString(); | ||
| var role = "admin"; | ||
|
|
||
| var userDetails = new UserDetails() | ||
| { | ||
| Email = email, | ||
| Name = name, | ||
| Id = id, | ||
| SessionId = sessionId, | ||
| Role = role, | ||
| }; | ||
| tracer.ActiveScope?.Span.SetUser(userDetails); | ||
|
|
||
| childTestScope.Dispose(); | ||
|
|
||
| Assert.Equal(email, rootTestScope.Span.GetTag(Tags.User.Email)); | ||
| Assert.Equal(name, rootTestScope.Span.GetTag(Tags.User.Name)); | ||
| Assert.Equal(id, rootTestScope.Span.GetTag(Tags.User.Id)); | ||
| Assert.Equal(sessionId, rootTestScope.Span.GetTag(Tags.User.SessionId)); | ||
| Assert.Equal(role, rootTestScope.Span.GetTag(Tags.User.Role)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetUser_ShouldWorkOnAnythingImplementingISpan() | ||
| { | ||
| var testSpan = new SpanStub(); | ||
|
|
||
| var email = "[email protected]"; | ||
| var name = "Jane Doh"; | ||
| var id = Guid.NewGuid().ToString(); | ||
| var sessionId = Guid.NewGuid().ToString(); | ||
| var role = "admin"; | ||
|
|
||
| var userDetails = new UserDetails() | ||
| { | ||
| Email = email, | ||
| Name = name, | ||
| Id = id, | ||
| SessionId = sessionId, | ||
| Role = role, | ||
| }; | ||
| testSpan.SetUser(userDetails); | ||
|
|
||
| Assert.Equal(email, testSpan.GetTag(Tags.User.Email)); | ||
| Assert.Equal(name, testSpan.GetTag(Tags.User.Name)); | ||
| Assert.Equal(id, testSpan.GetTag(Tags.User.Id)); | ||
| Assert.Equal(sessionId, testSpan.GetTag(Tags.User.SessionId)); | ||
| Assert.Equal(role, testSpan.GetTag(Tags.User.Role)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetUser_ShouldThrowAnExceptionIfNoIdIsProvided() | ||
| { | ||
| var testSpan = new SpanStub(); | ||
|
|
||
| var email = "[email protected]"; | ||
|
|
||
| var userDetails = new UserDetails() | ||
| { | ||
| Email = email, | ||
| }; | ||
|
|
||
| Assert.ThrowsAny<ArgumentException>(() => | ||
| testSpan.SetUser(userDetails)); | ||
| } | ||
|
|
||
| private class SpanStub : ISpan | ||
| { | ||
| private Dictionary<string, string> _tags = new Dictionary<string, string>(); | ||
|
|
||
| public string OperationName { get; set; } | ||
|
|
||
| public string ResourceName { get; set; } | ||
|
|
||
| public string Type { get; set; } | ||
|
|
||
| public bool Error { get; set; } | ||
|
|
||
| public string ServiceName { get; set; } | ||
|
|
||
| public ulong TraceId => 1ul; | ||
|
|
||
| public ulong SpanId => 1ul; | ||
|
|
||
| public ISpanContext Context => null; | ||
|
|
||
| public void Dispose() | ||
| { | ||
| } | ||
|
|
||
| public void Finish() | ||
| { | ||
| } | ||
|
|
||
| public void Finish(DateTimeOffset finishTimestamp) | ||
| { | ||
| } | ||
|
|
||
| public string GetTag(string key) | ||
| { | ||
| _tags.TryGetValue(key, out var value); | ||
| return value; | ||
| } | ||
|
|
||
| public void SetException(Exception exception) | ||
| { | ||
| } | ||
|
|
||
| public ISpan SetTag(string key, string value) | ||
| { | ||
| _tags[key] = value; | ||
| return this; | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.