Skip to content

Commit

Permalink
feat: Makes it easier to use tracing from non ASP.NET Core applications
Browse files Browse the repository at this point in the history
Closes #5897
Towards #6367
  • Loading branch information
amanda-tarafa committed May 27, 2021
1 parent b35b9ea commit c4c9cd5
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<ProjectReference Include="..\..\Google.Cloud.Diagnostics.Common\Google.Cloud.Diagnostics.Common.Tests\Google.Cloud.Diagnostics.Common.Tests.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Moq" Version="4.12.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ProjectReference Include="..\..\Google.Cloud.Diagnostics.Common\Google.Cloud.Diagnostics.Common.Tests\Google.Cloud.Diagnostics.Common.Tests.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Moq" Version="4.12.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2021 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.ClientTesting;
using Google.Cloud.Diagnostics.Common;
using Google.Cloud.Diagnostics.Common.IntegrationTests;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Diagnostics.AspNetCore.Snippets
{
public class StandaloneTraceSnippets
{
private static readonly string ProjectId = TestEnvironment.GetTestProjectId();

private static readonly TraceEntryPolling s_polling = new TraceEntryPolling();

private readonly string _testId;

private readonly Timestamp _startTime;

public StandaloneTraceSnippets()
{
_testId = IdGenerator.FromDateTime();
_startTime = Timestamp.FromDateTime(DateTime.UtcNow);

// The rate limiter instance is static and only set once. If we do not reset it at the
// beginning of each tests the qps will not change. This is dependent on the tests not
// running in parallel.
RateLimiter.Reset();
}

// Sample: Configure
public static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddGoogleTrace(options => options.ProjectId = ProjectId);
// Register other services here if you need them.
});
// End sample

[Fact]
public async Task TraceAsync()
{
// Naming it like an instance variable so that it looks like that on sample code.
IHost _host = null;

try
{
// Sample: Start
_host = CreateHostBuilder().Build();
await _host.StartAsync();
// End sample

// Sample: IncomingContext
ITraceContext traceContext = GetTraceContextFromIncomingRequest();
var tracerFactory = _host.Services.GetRequiredService<Func<ITraceContext, IManagedTracer>>();
IManagedTracer tracer = tracerFactory(traceContext);
ContextTracerManager.SetCurrentTracer(tracer);
// End sample

// Let's just start a span with the test ID so we can find it faster.
// But we don't show this in sample code.
using (tracer.StartSpan(_testId))
{
// Sample: Trace
IManagedTracer currentTracer = _host.Services.GetRequiredService<IManagedTracer>();
using (currentTracer.StartSpan("standalone_tracing"))
{
Console.WriteLine("Using Cloud Trace from a non ASP.NET Core app");
}
// End sample
}

var trace = s_polling.GetTrace(_testId, _startTime);
TraceEntryVerifiers.AssertParentChildSpan(trace, _testId, "standalone_tracing");
Assert.Equal(traceContext.TraceId, trace.TraceId);
}
finally
{
if (_host != null)
{
await _host.StopAsync();
}
}
}

private static ITraceContext GetTraceContextFromIncomingRequest() =>
new SimpleTraceContext(TraceIdFactory.Create().NextId(), null, true);
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ProjectReference Include="..\..\Google.Cloud.Diagnostics.Common\Google.Cloud.Diagnostics.Common.Tests\Google.Cloud.Diagnostics.Common.Tests.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Moq" Version="4.12.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@

using Google.Api.Gax;
using Google.Cloud.Diagnostics.Common;
using Google.Cloud.Trace.V1;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Net.Http;

#if NETCOREAPP3_1
namespace Google.Cloud.Diagnostics.AspNetCore3
Expand Down Expand Up @@ -112,32 +110,27 @@ public static IServiceCollection AddGoogleTrace(
var serviceOptions = new TraceServiceOptions();
setupAction(serviceOptions);

var client = serviceOptions.Client ?? TraceServiceClient.Create();
var options = serviceOptions.Options ?? TraceOptions.Create();
var traceFallbackPredicate = serviceOptions.TraceFallbackPredicate ?? TraceDecisionPredicate.Default;
var projectId = Project.GetAndCheckProjectId(serviceOptions.ProjectId);
services.AddGoogleTrace((Common.TraceServiceOptions commonOptions) =>
{
commonOptions.ProjectId = serviceOptions.ProjectId;
commonOptions.Options = serviceOptions.Options;
commonOptions.Client = serviceOptions.Client;
});

var consumer = ManagedTracer.CreateConsumer(client, options);
var tracerFactory = ManagedTracer.CreateFactory(projectId, consumer, options);
var traceFallbackPredicate = serviceOptions.TraceFallbackPredicate ?? TraceDecisionPredicate.Default;

// We use TryAdd... here to allow user code to inject their own trace context provider
// and matching trace context response propagator. We use Google trace header otherwise.
services.TryAddScoped<ITraceContext>(ProvideGoogleTraceHeaderContext);
services.TryAddSingleton<Action<HttpResponse, ITraceContext>>(PropagateGoogleTraceHeaders);

services.AddSingleton(tracerFactory);
// Obsolete: Adding this for backwards compatibility in case someone is using the old factory type.
// The new and prefered factory type is Func<ITraceContext, IManagedTracer>.
services.AddSingleton<Func<TraceHeaderContext, IManagedTracer>>(tracerFactory);
services.AddSingleton<Func<TraceHeaderContext, IManagedTracer>>(sp => sp.GetRequiredService<Func<ITraceContext, IManagedTracer>>());

services.AddHttpContextAccessor();
services.AddSingleton(ManagedTracer.CreateDelegatingTracer(ContextTracerManager.GetCurrentTracer));
services.AddTransient<ICloudTraceNameProvider, DefaultCloudTraceNameProvider>();

// On .Net Standard 2.0 or higher, we can use the System.Net.Http.IHttpClientFactory defined in Microsoft.Extensions.Http,
// for which we need a DelagatingHandler with no InnerHandler set. This is the recommended way.
// It should be registered as follows.
services.AddTransient(UnchainedTraceHeaderPropagatingHandlerFactory);

// This is to be used for explicitly creating an HttpClient instance. Valid for all platforms but subject to
// issues with HttpClient lifetime as described in
// https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests.
Expand Down Expand Up @@ -173,15 +166,5 @@ internal static void PropagateGoogleTraceHeaders(HttpResponse response, ITraceCo
var googleHeader = TraceHeaderContext.Create(traceContext.TraceId, traceContext.SpanId ?? 0, traceContext.ShouldTrace);
response.Headers.Add(TraceHeaderContext.TraceHeader, googleHeader.ToString());
}

/// <summary>
/// Returns an <see cref="UnchainedTraceHeaderPropagatingHandler"/> configured with the user specified trace context
/// outgoing propagator. If user code has not specified a trace context outgoing propagator, the Google header will
/// be propagated.
/// </summary>
internal static UnchainedTraceHeaderPropagatingHandler UnchainedTraceHeaderPropagatingHandlerFactory(IServiceProvider serviceProvider) =>
serviceProvider.GetService<Action<HttpRequestMessage, ITraceContext>>() is Action<HttpRequestMessage, ITraceContext> traceContextOutgoingPropagator ?
new UnchainedTraceHeaderPropagatingHandler(ContextTracerManager.GetCurrentTracer, traceContextOutgoingPropagator) :
new UnchainedTraceHeaderPropagatingHandler(ContextTracerManager.GetCurrentTracer);
}
}
31 changes: 31 additions & 0 deletions apis/Google.Cloud.Diagnostics.AspNetCore/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,34 @@ that is propagated in a `custom_trace_id` header. This is of no use in the real

{{sample:Trace.CustomTraceContext}}

## Using Google Trace in applications not based in ASP.NET Core

If you want to use Google Cloud Trace in applications not based on ASP.NET Core you may use the
`Google.Cloud.Diagnostics.Common` package directly and .NET's dependency injection mechanism.
You can read more about .NET dependency injection in non ASP.NET Core apps in the
[Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage).

Note that this is useful for both installed applications and services that process incoming messages
other than HTTP requests, such as a service reacting to Pub/Sub messages.

Following you'll see a very simplified example of how you could set up Google Cloud Trace for these
types of applications.

- Configure Google Cloud Trace. You can set tracing options the same as you would do for ASP.NET Core apps.

{{sample:StandaloneTrace.Configure}}

- Build and start a `Microsoft.Extensions.Hosting.IHost`.

{{sample:StandaloneTrace.Start}}

- Create a tracing context when appropiate, for instance, when you receive a Pub/Sub message. You can create
an empty tracing context (with all null values) if there's none. The tracer will create a tracing context
depending on configuration options like QPS, etc.

{{sample:StandaloneTrace.IncomingContext}}

- Trace normally in your code

{{sample:StandaloneTrace.Trace}}

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2021 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.Api.Gax;
using Google.Cloud.Trace.V1;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;

namespace Google.Cloud.Diagnostics.Common
{
/// <summary>
/// Extension methods for registering Google Cloud Trace for
/// dependency injection.
/// </summary>
public static class CloudTraceExtensions
{
/// <summary>
/// Configures Google Cloud Trace for dependency injection.
/// </summary>
public static IServiceCollection AddGoogleTrace(
this IServiceCollection services, Action<TraceServiceOptions> setupAction)
{
GaxPreconditions.CheckNotNull(services, nameof(services));
GaxPreconditions.CheckNotNull(setupAction, nameof(setupAction));

var serviceOptions = new TraceServiceOptions();
setupAction(serviceOptions);

var client = serviceOptions.Client ?? TraceServiceClient.Create();
var options = serviceOptions.Options ?? TraceOptions.Create();
var projectId = Project.GetAndCheckProjectId(serviceOptions.ProjectId);

var consumer = ManagedTracer.CreateConsumer(client, options);
var tracerFactory = ManagedTracer.CreateFactory(projectId, consumer, options);

services.AddSingleton(tracerFactory);
services.AddSingleton(ManagedTracer.CreateDelegatingTracer(ContextTracerManager.GetCurrentTracer));

// On .Net Standard 2.0 or higher, we can use the System.Net.Http.IHttpClientFactory defined in Microsoft.Extensions.Http,
// for which we need a DelegatingHandler with no InnerHandler set. This is the recommended way.
// It should be registered as follows.
return services.AddTransient(UnchainedTraceHeaderPropagatingHandlerFactory);
}

/// <summary>
/// Returns an <see cref="UnchainedTraceHeaderPropagatingHandler"/> configured with the user specified trace context
/// outgoing propagator. If user code has not specified a trace context outgoing propagator, the Google header will
/// be propagated.
/// </summary>
internal static UnchainedTraceHeaderPropagatingHandler UnchainedTraceHeaderPropagatingHandlerFactory(IServiceProvider serviceProvider) =>
serviceProvider.GetService<Action<HttpRequestMessage, ITraceContext>>() is Action<HttpRequestMessage, ITraceContext> traceContextOutgoingPropagator ?
new UnchainedTraceHeaderPropagatingHandler(ContextTracerManager.GetCurrentTracer, traceContextOutgoingPropagator) :
new UnchainedTraceHeaderPropagatingHandler(ContextTracerManager.GetCurrentTracer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2021 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.Trace.V1;

namespace Google.Cloud.Diagnostics.Common
{
/// <summary>
/// Options for initializing tracing.
/// </summary>
public sealed class TraceServiceOptions
{
/// <summary>
/// The Google Cloud Platform project ID. If unspecified and running on GAE or GCE the project
/// ID will be detected from the platform.
/// </summary>
public string ProjectId { get; set; }

/// <summary>
/// Trace options. May be null.
/// </summary>
public TraceOptions Options { get; set; }

/// <summary>
/// A client to send traces with. May be null.
/// </summary>
public TraceServiceClient Client { get; set; }
}
}
3 changes: 2 additions & 1 deletion apis/apis.json
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,8 @@
"Google.Cloud.Diagnostics.Common.IntegrationTests": "project",
"Google.Cloud.Diagnostics.Common.Tests": "project",
"Microsoft.AspNetCore.Mvc": "2.1.3",
"Microsoft.AspNetCore.TestHost": "2.1.1"
"Microsoft.AspNetCore.TestHost": "2.1.1",
"Microsoft.Extensions.Hosting": "3.0.0"
},
"additionalAnalyzerTestDependencies": {
"Microsoft.AspNetCore.Mvc.Core": "1.0.4",
Expand Down

0 comments on commit c4c9cd5

Please sign in to comment.