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

Added Console Trace link writer #222

Merged
merged 15 commits into from
Aug 1, 2022
106 changes: 106 additions & 0 deletions src/Honeycomb.OpenTelemetry/ConsoleLinksExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using OpenTelemetry;
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Honeycomb.OpenTelemetry
{
/// <summary>
/// Writes links to the Honeycomb UI for all root spans to the console
/// </summary>
public class ConsoleLinkExporter : BaseExporter<Activity>
{
private string _apiKey;
private string _authApiHost;
private string _serviceName;
private string _teamSlug;
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 getting the team and environment slugs could be reusable so may end up being passed in instead of being resolved inside this span processor.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that we should be calling out via http as part of startup in any other environment than production. It's an async call in a sync method, and I don't like it all. As it stands, because it's in the ConsoleLinkExporter, it won't be triggered unless you ask for tracelinks.

I think that's probably an internal decision too, so we can refactor that as and when we need it.

private string _environmentSlug;

private bool IsEnabled = false;

/// <summary>
/// Initializes the <see cref="ConsoleLinkExporter" /> class
/// </summary>
/// <param name="options">Settings for Link generation</param>
public ConsoleLinkExporter(HoneycombOptions options)
{
_apiKey = options.ApiKey;
_authApiHost = options.TracesEndpoint;
_serviceName = options.ServiceName;
InitTraceLinkParameters();
}

private void InitTraceLinkParameters()
{
if (string.IsNullOrEmpty(_apiKey))
return;

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(_authApiHost);
httpClient.DefaultRequestHeaders.Add("X-Honeycomb-Team", _apiKey);

var response = httpClient.GetAsync("/1/auth").GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode) {
Console.WriteLine("Didn't get a valid response from Honeycomb");
return;
}

var responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var authResponse = JsonSerializer.Deserialize<AuthResponse>(responseString);
_environmentSlug = authResponse.Environment.Slug;
_teamSlug = authResponse.Team.Slug;
if (string.IsNullOrEmpty(_environmentSlug) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

We can generate trace links for classic datasets, it just needs to omit the environment segments.

Copy link
Member Author

Choose a reason for hiding this comment

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

How would we detect if it's a classic dataset?

Copy link
Contributor

Choose a reason for hiding this comment

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

How would we detect if it's a classic dataset?

Classic (nee Legacy) API key is 32 characters:

string.IsNullOrEmpty(_teamSlug)) {
Console.WriteLine("Team or Environment wasn't returned");
return;
}
IsEnabled = true;
}

/// <inheritdoc />
public override ExportResult Export(in Batch<Activity> batch)
{
if (!IsEnabled)
return ExportResult.Success;

foreach (var activity in batch)
{
if (string.IsNullOrEmpty(activity.ParentId))
{
Console.WriteLine($"Trace Emitted for {activity.DisplayName}");
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to expand this in the future to write to an ILogger instead of directly to console. Then users can select where they want these to appear.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually don't want it going through the logging framework, as that could take it into other pipelines, and this is specifically for local development.

Console.WriteLine($"TraceLink: {GetTraceLink(activity.TraceId.ToString())}");
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 it would be nice to combine these into a single line for easier searching and reduce screen congestion for higher volume apps.

Copy link
Member Author

Choose a reason for hiding this comment

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

Keep in mind that this is just for the root span, not all spans, so even in large volume applications, we should only see a small amount of spans.

1 line is actually pretty long as the url is already quite long, which is why I extended it to 2. Happy to hear thoughts though, I'm not 100% convinced this is right.

}
}
return ExportResult.Success;
}

private string GetTraceLink(string traceId)
{
return $"http://ui.honeycomb.io/{_teamSlug}/environments/{_environmentSlug}/datasets/{_serviceName}/trace?trace_id={traceId}";
}
}

#pragma warning disable 1591
public class AuthResponse
{
[JsonPropertyName("environment")]
public HoneycombEnvironment Environment { get; set; }

[JsonPropertyName("team")]
public Team Team { get; set; }
}
public class HoneycombEnvironment
{
[JsonPropertyName("slug")]
public string Slug { get; set; }
}

public class Team
{
[JsonPropertyName("slug")]
public string Slug { get; set; }
}

}
2 changes: 2 additions & 0 deletions src/Honeycomb.OpenTelemetry/EnvironmentOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class EnvironmentOptions
private const string SampleRateKey = "HONEYCOMB_SAMPLE_RATE";
private const string ServiceNameKey = "SERVICE_NAME";
private const string ServiceVersionKey = "SERVICE_VERSION";
private const string WriteTraceLinksToConsoleKey = "WRITE_TRACE_LINKS_TO_CONSOLE";
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels long, maybe LOG_TRACE_LINKS. If we do go along the route of using an ILogger in the future, these trace links may go elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

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

using Console is more universal I think, and also keeps it specific to enviroments where you have access to the console.

Copy link
Contributor

@vreynolds vreynolds Jul 25, 2022

Choose a reason for hiding this comment

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

in zoom convo, we thought ENABLE_LOCAL_VISUALIZATIONS would work nicely, as a more generic "mode" setting that is not too specific with what we're writing where

private const uint DefaultSampleRate = 1;
private const string DefaultApiEndpoint = "https://api.honeycomb.io:443";
private readonly IDictionary _environmentService;
Expand All @@ -36,6 +37,7 @@ internal EnvironmentOptions(IDictionary service)
internal string MetricsEndpoint => GetEnvironmentVariable(MetricsEndpointKey, ApiEndpoint);
internal string ServiceName => GetEnvironmentVariable(ServiceNameKey);
internal string ServiceVersion => GetEnvironmentVariable(ServiceVersionKey);
internal bool WriteTraceLinksToConsole => bool.TryParse(GetEnvironmentVariable(WriteTraceLinksToConsoleKey), out var writeTraceLinksToConsole) ? writeTraceLinksToConsole : false;
internal uint SampleRate => uint.TryParse(GetEnvironmentVariable(SampleRateKey), out var sampleRate) ? sampleRate : DefaultSampleRate;

private string GetEnvironmentVariable(string key, string defaultValue = "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
</ItemGroup>

<Choose>
Expand Down
14 changes: 13 additions & 1 deletion src/Honeycomb.OpenTelemetry/HoneycombOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class HoneycombOptions
private string _tracesDataset;
private string _tracesEndpoint;
private string _metricsEndpoint;
private bool _writeTraceLinksToConsole;

/// <summary>
/// Name of the Honeycomb section of IConfiguration
Expand Down Expand Up @@ -68,6 +69,15 @@ internal bool IsMetricsLegacyKey()
return MetricsApiKey?.Length == 32;
}

/// <summary>
/// Write links to honeycomb traces as they come in
/// </summary>
public bool WriteTraceLinksToConsole
Copy link
Contributor

Choose a reason for hiding this comment

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

Should match env var above.

{
get { return _writeTraceLinksToConsole; }
set { _writeTraceLinksToConsole = value; }
}

/// <summary>
/// API key used to send trace telemetry data to Honeycomb. Defaults to <see cref="ApiKey"/>.
/// </summary>
Expand Down Expand Up @@ -193,7 +203,8 @@ public string MetricsEndpoint
/// (Optional) Options delegate to configure StackExchange.Redis instrumentation.
/// </summary>
public Action<StackExchangeRedisCallsInstrumentationOptions>
ConfigureStackExchangeRedisClientInstrumentationOptions { get; set; }
ConfigureStackExchangeRedisClientInstrumentationOptions
{ get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this intentional, or did your IDE move this?


/// <summary>
/// (Optional) Additional <see cref="Meter"/> names for generating metrics.
Expand All @@ -220,6 +231,7 @@ public Action<StackExchangeRedisCallsInstrumentationOptions>
{ "--honeycomb-traces-endpoint", "tracesendpoint" },
{ "--honeycomb-metrics-endpoint", "metricsendpoint" },
{ "--honeycomb-samplerate", "samplerate" },
{ "--honeycomb-write-trace-links-to-console", "writetracelinkstoconsole" },
Copy link
Contributor

Choose a reason for hiding this comment

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

Should match env var

{ "--service-name", "servicename" },
{ "--service-version", "serviceversion" },
{ "--instrument-http", "instrumenthttpclient" },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;

#if NET461
using System.Collections.Generic;
using OpenTelemetry;
#endif

namespace Honeycomb.OpenTelemetry
Expand Down Expand Up @@ -72,6 +72,10 @@ public static TracerProviderBuilder AddHoneycomb(this TracerProviderBuilder buil
Console.WriteLine($"WARN: {EnvironmentOptions.GetErrorMessage("API Key", "HONEYCOMB_API_KEY")}.");
}

if (options.WriteTraceLinksToConsole) {
builder.AddProcessor(new SimpleActivityExportProcessor(new ConsoleLinkExporter(options)));
}

// heads up: even if dataset is set, it will be ignored
if (!string.IsNullOrWhiteSpace(options.TracesApiKey) & !options.IsTracesLegacyKey() & (!string.IsNullOrWhiteSpace(options.TracesDataset))) {
if (!string.IsNullOrWhiteSpace(options.ServiceName)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public void Can_get_options_from_env_vars()
{"HONEYCOMB_METRICS_ENDPOINT", "my-metrics-endpoint"},
{"HONEYCOMB_SAMPLE_RATE", "10"},
{"SERVICE_NAME", "my-service-name"},
{"SERVICE_VERSION", "my-service-version"}
{"SERVICE_VERSION", "my-service-version"},
{"WRITE_TRACE_LINKS_TO_CONSOLE", "true" }
};
var options = new EnvironmentOptions(values);
Assert.Equal("my-api-key", options.ApiKey);
Expand All @@ -36,6 +37,7 @@ public void Can_get_options_from_env_vars()
Assert.Equal((uint) 10, options.SampleRate);
Assert.Equal("my-service-name", options.ServiceName);
Assert.Equal("my-service-version", options.ServiceVersion);
Assert.Equal(true, options.WriteTraceLinksToConsole);
}

[Fact]
Expand All @@ -50,6 +52,7 @@ public void Optional_args_fall_back_to_defaults()
Assert.Equal(options.ApiEndpoint, options.TracesEndpoint);
Assert.Equal(options.ApiEndpoint, options.MetricsEndpoint);
Assert.Equal((uint) 1, options.SampleRate);
Assert.Equal(false, options.WriteTraceLinksToConsole);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public void CanParseOptionsFromConfiguration()
Assert.False(options.InstrumentSqlClient);
Assert.False(options.InstrumentGrpcClient);
Assert.False(options.InstrumentStackExchangeRedisClient);
Assert.True(options.WriteTraceLinksToConsole);
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion test/Honeycomb.OpenTelemetry.Tests/appsettings.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"InstrumentHttpClient": false,
"InstrumentSqlClient": "false",
"InstrumentGrpcClient": false,
"InstrumentStackExchangeRedisClient": "false"
"InstrumentStackExchangeRedisClient": "false",
"WriteTraceLinksToConsole": true
}
}