Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.env
bin
obj
obj
.idea
*.DotSettings.user
40 changes: 40 additions & 0 deletions Controllers/TestController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using DevCycle.SDK.Server.Common.Model;
using Dynatrace.OneAgent.Sdk.Api;
using Newtonsoft.Json.Linq;

namespace HelloTogglebot.Controllers;

[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
private readonly ILogger<TestController> _logger;

public TestController(ILogger<TestController> logger)
{
_logger = logger;
}

[HttpGet("trace")]
public async Task<IActionResult> TestTraceAsync([FromServices] IOneAgentSdk oneAgent)
{

var client = DevCycleClient.GetClient();
var user = new DevCycleUser("userId");
var defaultValue = Newtonsoft.Json.Linq.JObject.Parse("{\"key\": \"default\"}");
var variable = await client.VariableAsync(user, "test", false);
var variable2 = await client.VariableAsync(user, "json-correct", defaultValue);
var variable3 = await client.VariableAsync(user, "test-string", "default");
var variable4 = await client.VariableAsync(user, "test-number", 9);
var variable5 = await client.VariableAsync(user, "togglebot-wink", true);

return Ok(new
{
message = "Test trace created",
sdkState = oneAgent.CurrentState.ToString(),
timestamp = DateTimeOffset.UtcNow,
variable = variable
});
}
}
15 changes: 10 additions & 5 deletions HelloTogglebot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
<RootNamespace>HelloTogglebot</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DevCycle.SDK.Server.Local" Version="4.0.4" />
</ItemGroup>

</Project>
<ItemGroup>
<PackageReference Include="DevCycle.SDK.Server.Local" Version="4.8.1" />
<PackageReference Include="Dynatrace.OneAgent.Sdk" Version="1.8.0" />
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
</ItemGroup>
</Project>
83 changes: 83 additions & 0 deletions Hooks/ActivityHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Diagnostics;
using System.Collections.Concurrent;
using DevCycle.SDK.Server.Common.Model;

namespace HelloTogglebot.Hooks
{
public class ActivityHook : EvalHook
{
private readonly ActivitySource _activitySource;
private readonly ConcurrentDictionary<string, Activity> _activities = new();

public ActivityHook(ActivitySource activitySource)
{
_activitySource = activitySource;
}

public override async Task<HookContext<T>> BeforeAsync<T>(HookContext<T> context, CancellationToken cancellationToken = default)
{
var activity = _activitySource.StartActivity($"feature_flag_evaluation.{context.Key}");

if (activity != null)
{
var activityKey = $"{context.Key}_{context.User.UserId}";

activity.SetTag("feature_flag.key", context.Key);
activity.SetTag("feature_flag.provider.name", "devcycle");
activity.SetTag("feature_flag.context.id", context.User.UserId);
activity.SetTag("feature_flag.value_type", context.DefaultValue.GetType().Name);

if (context.Metadata != null)
{
activity.SetTag("feature_flag.project", context.Metadata.Project?.Id);
activity.SetTag("feature_flag.environment", context.Metadata.Environment?.Id);
}

_activities.TryAdd(activityKey, activity);
}

return await Task.FromResult(context);
}

public override Task AfterAsync<T>(HookContext<T> context, Variable<T> variableDetails, VariableMetadata variableMetadata, CancellationToken cancellationToken = default)
{
var activityKey = $"{context.Key}_{context.User.UserId}";
if (_activities.TryGetValue(activityKey, out var activity))
{
activity.SetTag("feature_flag.result.value", variableDetails.Value.ToString());
if (variableMetadata.FeatureId != null)
{
activity.SetTag("feature_flag.set.id", variableMetadata.FeatureId);
activity.SetTag("feature_flag.url", $"https://app.devcycle.com/r/p/{context.Metadata.Project.Id}/f/{variableMetadata.FeatureId}");
}
if (variableDetails.Eval != null)
{
activity.SetTag("feature_flag.result.reason", variableDetails.Eval.Reason);
activity.SetTag("feature_flag.result.reason.details", variableDetails.Eval.Details);
}
}
return Task.CompletedTask;
}

public override Task ErrorAsync<T>(HookContext<T> context, System.Exception error, CancellationToken cancellationToken = default)
{
var activityKey = $"{context.Key}_{context.User.UserId}";
if (_activities.TryGetValue(activityKey, out var activity))
{
activity.SetTag("feature_flag.error_message", error.Message);
activity.SetTag("error.type", error.GetType().Name);
}
return Task.CompletedTask;
}

public override Task FinallyAsync<T>(HookContext<T> context, Variable<T> variableDetails, VariableMetadata variableMetadata, CancellationToken cancellationToken = default)
{
var activityKey = $"{context.Key}_{context.User.UserId}";
if (_activities.TryRemove(activityKey, out var activity))
{
activity.Stop();
}
return Task.CompletedTask;
}
}
}
69 changes: 69 additions & 0 deletions Hooks/LogHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using DevCycle.SDK.Server.Common.Model;

namespace HelloTogglebot.Hooks
{
public class LogHook : EvalHook
{
private readonly ILogger _logger;

public LogHook(ILogger logger)
{
_logger = logger;
}

public override Task ErrorAsync<T>(HookContext<T> context, System.Exception error, CancellationToken cancellationToken = default)
{
var attributes = SetContextAttributes<T>(context);
attributes.Add("feature_flag.error_message", error.Message);
attributes.Add("error.type", error.GetType().Name);

using (_logger.BeginScope(attributes))
{
_logger.LogError($"Error evaluating flag: {context.Key}");
}
return Task.CompletedTask;
}

public override Task AfterAsync<T>(HookContext<T> context, Variable<T> variableDetails, VariableMetadata variableMetadata, CancellationToken cancellationToken = default)
{
var attributes = SetContextAttributes<T>(context);
attributes.Add("feature_flag.result.value", variableDetails.Value.ToString() ?? "");
if (variableMetadata.FeatureId != null)
{
attributes.Add("feature_flag.set.id", variableMetadata.FeatureId);
attributes.Add("feature_flag.url", $"https://app.devcycle.com/r/p/{context.Metadata?.Project.Id}/f/{variableMetadata.FeatureId}");
}
if (variableDetails.Eval != null)
{
attributes.Add("feature_flag.result.reason", variableDetails.Eval.Reason);
attributes.Add("feature_flag.result.reason.details", variableDetails.Eval.Details);
}

using (_logger.BeginScope(attributes))
{
_logger.LogInformation($"Feature flag evaluated {context.Key}");
}
return Task.CompletedTask;
}

private IDictionary<string, object> SetContextAttributes<T>(HookContext<T> context)
{
var attributes = new Dictionary<string, object>
{
["feature_flag.key"] = context.Key,
["feature_flag.provider.name"] = "devcycle",
["feature_flag.context.id"] = context.User.UserId,
};
if (context.DefaultValue != null)
{
attributes.Add("feature_flag.value_type", context.DefaultValue.GetType().Name);
}
if (context.Metadata != null)
{
attributes.Add("feature_flag.project", context.Metadata.Project.Id);
attributes.Add("feature_flag.environment", context.Metadata.Environment.Id);
}
return attributes;
}
}
}
20 changes: 14 additions & 6 deletions src/DevCycleClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,33 @@ public static async Task Initialize()
.SetOptions(
new DevCycleLocalOptions(configPollingIntervalMs: 5000, eventFlushIntervalMs: 1000)
)
.SetInitializedSubscriber((o, e) => {
if (e.Success) {
.SetInitializedSubscriber((o, e) =>
{
if (e.Success)
{
initialized = true;
} else {
}
else
{
Console.WriteLine($"DevCycle Client did not initialize. Errors: {e.Errors}");
}
})
.Build();

try {
try
{
await Task.Delay(5000);
} catch (TaskCanceledException) {
}
catch (TaskCanceledException)
{
System.Environment.Exit(0);
}
}

public static DevCycleLocalClient GetClient()
{
if (!initialized || client == null) {
if (!initialized || client == null)
{
throw new Exception("DevCycle Client not initialized");
}
return client;
Expand Down
Loading