diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs index 616be170e369..e493391375c2 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.IntegrationTests/Logging/LoggingTest.cs @@ -29,6 +29,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -500,7 +501,7 @@ public static object[][] ExternalTraceBuilders return new object[][] { new object[] { new WebHostBuilder().UseStartup() }, - new object[] { new WebHostBuilder().UseStartup() } + new object[] { new WebHostBuilder().UseStartup() } }; } } @@ -567,6 +568,74 @@ public async Task Logging_Trace_External_MultipleEntries(IWebHostBuilder builder }); } + [Fact] + public async Task Logging_Trace_GoogleExternal() + { + Timestamp startTime = Timestamp.FromDateTime(DateTime.UtcNow); + string testId = IdGenerator.FromGuid(); + + string traceId = "105445aa7843bc8bf206b12000100f00"; + ulong spanId = 0x12D687; + // The spanId set on the log entry should confirm to x16 + // format so that the backend can really associate the log entry + // to the span. + string expectedSpanId = "000000000012d687"; + + string url = $"/Main/Critical/{testId}"; + var builder = new WebHostBuilder().UseStartup(); + using (var server = new TestServer(builder)) + using (var client = server.CreateClient()) + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + // Set the Google tracing header so that it can be read by the + // GoogleTraceProvider. This is the same that GCP would do. + // Note that we are actually not creating the trace on Google Trace, + // which is fine for the purposes of this test. + string traceHeaderValue = $"{traceId}/{spanId};o=1"; + request.Headers.Add("X-Cloud-Trace-Context", traceHeaderValue); + await client.SendAsync(request); + } + + _fixture.AddValidator(testId, results => + { + // We only have one log entry. + LogEntry entry = Assert.Single(results); + + // And the resource name of the trace associated to it contains the external trace id. + Assert.Contains(TestEnvironment.GetTestProjectId(), entry.Trace); + Assert.Contains(traceId, entry.Trace); + + // The span associated to our entry is the external span. + Assert.Equal(expectedSpanId, entry.SpanId); + }); + } + + [Fact] + public async Task Logging_Trace_GoogleExternal_NoTraceHeader() + { + Timestamp startTime = Timestamp.FromDateTime(DateTime.UtcNow); + string testId = IdGenerator.FromGuid(); + + string url = $"/Main/Critical/{testId}"; + var builder = new WebHostBuilder().UseStartup(); + using (var server = new TestServer(builder)) + using (var client = server.CreateClient()) + { + await client.GetAsync(url); + } + + _fixture.AddValidator(testId, results => + { + // We only have one log entry. + LogEntry entry = Assert.Single(results); + + // The entry does not have any trace information. + // These are empty strings instead of null. + Assert.Equal("", entry.Trace); + Assert.Equal("", entry.SpanId); + }); + } + [Fact] public async Task Logging_Labels() { @@ -744,7 +813,7 @@ public void Configure(IApplicationBuilder app) => /// /// A simple web application to test the and associated classes. /// - public class LoggerNoTracingActivatedTestApplication + public abstract class LoggerNoTracingActivatedTestApplication { protected readonly string _projectId = TestEnvironment.GetTestProjectId(); @@ -752,7 +821,6 @@ public virtual void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddMvc(); - services.AddSingleton(new SpanCountingExternalTraceProvider()); } public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) @@ -769,6 +837,24 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) } } + public class ExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication + { + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddSingleton(); + } + } + + public class GoogleTraceAsExternalTracingTestApplication : LoggerNoTracingActivatedTestApplication + { + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddSingleton(); + } + } + /// /// A simple web application to test the and associated classes. /// @@ -798,7 +884,7 @@ public virtual void ConfigureServices(IServiceCollection services) public void SetupRoutes(IApplicationBuilder app) { app.UseGoogleTrace() - .UseMvc(routes => + .UseMvc(routes => { routes.MapRoute( name: "default", diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs new file mode 100644 index 000000000000..9e1387adabb5 --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore.Tests/Logging/GoogleTraceProviderTests.cs @@ -0,0 +1,80 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Cloud.Diagnostics.Common; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Moq; +using System; +using Xunit; + +namespace Google.Cloud.Diagnostics.AspNetCore.Tests +{ + public class GoogleTraceProviderTests + { + [Fact] + public void GetCurrentTraceContext() + { + string traceId = "105445aa7843bc8bf206b12000100f00"; + ulong spanId = 0x12D687; + // The spanId set on the log entry should confirm to x16 + // format so that the backend can really associate the log entry + // to the span. + string expectedSpanId = "000000000012d687"; + + IServiceProvider serviceProvider = MockServiceProvider(traceId, spanId, true); + + GoogleTraceProvider traceProvider = new GoogleTraceProvider(); + TraceContextForLogEntry traceContext = traceProvider.GetCurrentTraceContext(serviceProvider); + + Assert.Equal(traceId, traceContext.TraceId); + Assert.Equal(expectedSpanId, traceContext.SpanId); + } + + [Fact] + public void GetCurrentTraceContext_ShouldNotTrace() + { + string traceId = "105445aa7843bc8bf206b12000100f00"; + ulong spanId = 1234567; + + IServiceProvider serviceProvider = MockServiceProvider(traceId, spanId, false); + + GoogleTraceProvider traceProvider = new GoogleTraceProvider(); + Assert.Null(traceProvider.GetCurrentTraceContext(serviceProvider)); + } + + private IServiceProvider MockServiceProvider(string traceId, ulong spanId, bool shouldTrace) + { + char shouldTraceBit = shouldTrace ? '1' : '0'; + StringValues headerValue = $"{traceId}/{spanId};o={shouldTraceBit}"; + + Mock headerDictionaryMock = new Mock(MockBehavior.Strict); + headerDictionaryMock.Setup(hd => hd.TryGetValue(TraceHeaderContext.TraceHeader, out headerValue)).Returns(true); + + Mock requestMock = new Mock(MockBehavior.Strict); + requestMock.Setup(r => r.Headers).Returns(headerDictionaryMock.Object); + + Mock contextMock = new Mock(MockBehavior.Strict); + contextMock.Setup(c => c.Request).Returns(requestMock.Object); + + Mock contextAccessorMock = new Mock(MockBehavior.Strict); + contextAccessorMock.Setup(a => a.HttpContext).Returns(contextMock.Object); + + Mock serviceProviderMock = new Mock(MockBehavior.Strict); + serviceProviderMock.Setup(p => p.GetService(typeof(IHttpContextAccessor))).Returns(contextAccessorMock.Object); + + return serviceProviderMock.Object; + } + } +} diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs new file mode 100644 index 000000000000..68529753dc2d --- /dev/null +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/GoogleTraceProvider.cs @@ -0,0 +1,36 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Google.Cloud.Diagnostics.AspNetCore +{ + /// + /// If this is registered as a dependency, then Log Entries will be associated with + /// the Google trace and span. + /// + /// + /// To be used when the Tracing component of the Google.Cloud.Diagnostics libraries + /// is not configured, but Google traces are still being generated, for instance, + /// because the application is being run in Google Cloud. + /// If the Tracing component is configured, log entries are automatically associated + /// to Google traces and spans. + /// + public class GoogleTraceProvider : IExternalTraceProvider + { + /// + public TraceContextForLogEntry GetCurrentTraceContext(IServiceProvider serviceProvider) => + TraceContextForLogEntry.FromGoogleTraceHeader(serviceProvider); + } +} diff --git a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs index cacc3cc5cf73..cbc04104298d 100644 --- a/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs +++ b/apis/Google.Cloud.Diagnostics.AspNetCore/Google.Cloud.Diagnostics.AspNetCore/Logging/TraceContextForLogEntry.cs @@ -14,7 +14,9 @@ using Google.Api.Gax; using Google.Cloud.Diagnostics.Common; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using System; namespace Google.Cloud.Diagnostics.AspNetCore @@ -59,6 +61,21 @@ internal static TraceContextForLogEntry FromGoogleTrace() => new TraceContextForLogEntry(traceId, SpanIdToHex(tracer.GetCurrentSpanId())) : null; + internal static TraceContextForLogEntry FromGoogleTraceHeader(IServiceProvider serviceProvider) + { + if (serviceProvider?.GetService()? + .HttpContext.Request.Headers + .TryGetValue(TraceHeaderContext.TraceHeader, out StringValues headerValue) == true) + { + var traceContext = TraceHeaderContext.FromHeader(headerValue); + if (traceContext.ShouldTrace == true) + { + return new TraceContextForLogEntry(traceContext.TraceId, SpanIdToHex(traceContext.SpanId)); + } + } + return null; + } + internal static TraceContextForLogEntry FromExternalTrace(IServiceProvider serviceProvider) => serviceProvider?.GetService()?.GetCurrentTraceContext(serviceProvider);