diff --git a/README.md b/README.md index d772661..f607cbb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ These are the client libraries that instrument developer tooling and send teleme | `agoda-devfeedback-rsbuild` | npm | `POST /vite` | [devfeedback-js](https://github.com/agoda-com/devfeedback-js) | | JUnit reporter | Maven | `POST /junit` | [java-local-metrics](https://github.com/agoda-com/java-local-metrics) | | ScalaTest reporter | sbt | `POST /scala/scalatest` | [java-local-metrics](https://github.com/agoda-com/java-local-metrics) | +| Gradle build metrics plugin | Gradle | `POST /gradle` | [kotlin-local-metrics](https://github.com/agoda-com/kotlin-local-metrics) | +| Ktor startup metrics plugin | Gradle | `POST /ktor` | [kotlin-local-metrics](https://github.com/agoda-com/kotlin-local-metrics) | | Talaiot Gradle plugin | Gradle | `POST /gradletalaiot` | [Talaiot](https://github.com/cdsap/Talaiot) (external) | | Jest reporter | npm | `POST /jest` | [testresults-collector](https://github.com/agoda-com/testresults-collector) | | Vitest reporter | npm | `POST /vitest` | [testresults-collector](https://github.com/agoda-com/testresults-collector) | diff --git a/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleBuildMetricPayload.cs b/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleBuildMetricPayload.cs new file mode 100644 index 0000000..c3c6bf9 --- /dev/null +++ b/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleBuildMetricPayload.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace Agoda.DevExTelemetry.Core.Models.Ingest; + +public class GradleBuildMetricPayload : JvmBuildMetricPayload +{ + [JsonPropertyName("ide")] + public string? Ide { get; set; } + + [JsonPropertyName("buildKind")] + public string? BuildKind { get; set; } + + [JsonPropertyName("requestedTasks")] + public List? RequestedTasks { get; set; } + + [JsonPropertyName("taskCount")] + public int TaskCount { get; set; } + + [JsonPropertyName("executedTaskCount")] + public int ExecutedTaskCount { get; set; } + + [JsonPropertyName("upToDateTaskCount")] + public int UpToDateTaskCount { get; set; } + + [JsonPropertyName("fromCacheTaskCount")] + public int FromCacheTaskCount { get; set; } + + [JsonPropertyName("failedTaskCount")] + public int FailedTaskCount { get; set; } + + [JsonPropertyName("compileTaskCount")] + public int CompileTaskCount { get; set; } + + [JsonPropertyName("compileTimeMs")] + public long CompileTimeMs { get; set; } + + [JsonPropertyName("taskTimeMs")] + public long TaskTimeMs { get; set; } + + [JsonPropertyName("projects")] + public List? Projects { get; set; } +} diff --git a/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleProjectMetricPayload.cs b/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleProjectMetricPayload.cs new file mode 100644 index 0000000..aa6b50e --- /dev/null +++ b/src/Agoda.DevExTelemetry.Core/Models/Ingest/GradleProjectMetricPayload.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Agoda.DevExTelemetry.Core.Models.Ingest; + +public class GradleProjectMetricPayload +{ + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } + + [JsonPropertyName("compileTaskCount")] + public int CompileTaskCount { get; set; } + + [JsonPropertyName("compileTimeMs")] + public long CompileTimeMs { get; set; } +} diff --git a/src/Agoda.DevExTelemetry.Core/Models/Ingest/JvmBuildMetricPayload.cs b/src/Agoda.DevExTelemetry.Core/Models/Ingest/JvmBuildMetricPayload.cs new file mode 100644 index 0000000..f5eab78 --- /dev/null +++ b/src/Agoda.DevExTelemetry.Core/Models/Ingest/JvmBuildMetricPayload.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Agoda.DevExTelemetry.Core.Models.Ingest; + +public abstract class JvmBuildMetricPayload +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("metricsVersion")] + public string? MetricsVersion { get; set; } + + [JsonPropertyName("userName")] + public string? UserName { get; set; } + + [JsonPropertyName("cpuCount")] + public int CpuCount { get; set; } + + [JsonPropertyName("hostname")] + public string? Hostname { get; set; } + + [JsonPropertyName("platform")] + public string? Platform { get; set; } + + [JsonPropertyName("os")] + public string? Os { get; set; } + + [JsonPropertyName("timeTaken")] + public string? TimeTaken { get; set; } + + [JsonPropertyName("branch")] + public string? Branch { get; set; } + + [JsonPropertyName("commitSha")] + public string? CommitSha { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("projectName")] + public string? ProjectName { get; set; } + + [JsonPropertyName("repository")] + public string? Repository { get; set; } + + [JsonPropertyName("repositoryName")] + public string? RepositoryName { get; set; } + + [JsonPropertyName("date")] + public string? Date { get; set; } + + [JsonPropertyName("isDebuggerAttached")] + public bool IsDebuggerAttached { get; set; } + + [JsonExtensionData] + public Dictionary? AdditionalData { get; set; } +} diff --git a/src/Agoda.DevExTelemetry.Core/Models/Ingest/KtorStartupMetricPayload.cs b/src/Agoda.DevExTelemetry.Core/Models/Ingest/KtorStartupMetricPayload.cs new file mode 100644 index 0000000..797f792 --- /dev/null +++ b/src/Agoda.DevExTelemetry.Core/Models/Ingest/KtorStartupMetricPayload.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Agoda.DevExTelemetry.Core.Models.Ingest; + +public class KtorStartupMetricPayload : JvmBuildMetricPayload +{ + [JsonPropertyName("tags")] + public Dictionary? Tags { get; set; } +} diff --git a/src/Agoda.DevExTelemetry.IntegrationTests/CustomWebApplicationFactory.cs b/src/Agoda.DevExTelemetry.IntegrationTests/CustomWebApplicationFactory.cs index 8e27a0e..9508d2e 100644 --- a/src/Agoda.DevExTelemetry.IntegrationTests/CustomWebApplicationFactory.cs +++ b/src/Agoda.DevExTelemetry.IntegrationTests/CustomWebApplicationFactory.cs @@ -13,16 +13,24 @@ namespace Agoda.DevExTelemetry.IntegrationTests; public class CustomWebApplicationFactory : WebApplicationFactory { private readonly DatabaseProvider _provider; + private readonly bool _enableRawPayloadStorage; private SqliteConnection? _sqliteConnection; private string? _pgDatabaseName; - public CustomWebApplicationFactory(DatabaseProvider provider = DatabaseProvider.Sqlite) + public CustomWebApplicationFactory( + DatabaseProvider provider = DatabaseProvider.Sqlite, + bool enableRawPayloadStorage = false) { _provider = provider; + _enableRawPayloadStorage = enableRawPayloadStorage; } protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.UseSetting( + "DataRetention:EnableRawPayloadStorage", + _enableRawPayloadStorage.ToString()); + if (_provider == DatabaseProvider.Sqlite) { _sqliteConnection = new SqliteConnection("Data Source=:memory:"); diff --git a/src/Agoda.DevExTelemetry.IntegrationTests/GradleBuildMetricIngestTests.cs b/src/Agoda.DevExTelemetry.IntegrationTests/GradleBuildMetricIngestTests.cs new file mode 100644 index 0000000..9395247 --- /dev/null +++ b/src/Agoda.DevExTelemetry.IntegrationTests/GradleBuildMetricIngestTests.cs @@ -0,0 +1,124 @@ +using System.Net; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace Agoda.DevExTelemetry.IntegrationTests; + +[TestFixture(DatabaseProvider.Sqlite)] +[TestFixture(DatabaseProvider.PostgreSql)] +public class GradleBuildMetricIngestTests +{ + private readonly DatabaseProvider _provider; + private CustomWebApplicationFactory _factory = null!; + private HttpClient _client = null!; + + public GradleBuildMetricIngestTests(DatabaseProvider provider) => _provider = provider; + + [SetUp] + public void SetUp() + { + _factory = new CustomWebApplicationFactory(_provider, enableRawPayloadStorage: true); + _client = _factory.CreateClient(); + } + + [TearDown] + public void TearDown() + { + _client.Dispose(); + _factory.Dispose(); + } + + [Test] + public async Task POST_Gradle_WithFullContract_QueuesCompleteBuildMetric() + { + var payload = TestFixtures.CreateGradleBuildMetricPayload(); + var response = await TestFixtures.PostJsonAsync(_client, "/gradle", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + await _factory.DrainBackgroundQueuesAsync(); + + using var db = _factory.CreateDbContext(); + var metric = await db.BuildMetrics.SingleAsync(); + metric.MetricType.ShouldBe("Gradle"); + metric.TimeTakenMs.ShouldBe(1842); + metric.BuildCategory.ShouldBe("API"); + metric.ReloadType.ShouldBeNull(); + metric.UserName.ShouldBe("developer"); + metric.CpuCount.ShouldBe(12); + metric.Hostname.ShouldBe("workstation"); + metric.Platform.ShouldBe("Linux"); + metric.Os.ShouldBe("Linux 6.8.0 amd64"); + metric.Branch.ShouldBe("feature/build-metrics"); + metric.ProjectName.ShouldBe("sample-service"); + metric.Repository.ShouldBe("https://example.com/group/sample-service.git"); + metric.RepositoryName.ShouldBe("sample-service"); + metric.ToolVersion.ShouldBe("0.1.0"); + metric.CommitSha.ShouldBe("6f9c4bd4a2d0f5cfa3fa8e199cc0c630f56a9898"); + metric.IsDebuggerAttached.ShouldBeFalse(); + metric.ExecutionEnvironment.ShouldBe("Local"); + metric.SourceEndpoint.ShouldBe("/gradle"); + + using var extraData = JsonDocument.Parse(metric.ExtraData!); + extraData.RootElement.GetProperty("date").GetString() + .ShouldBe("2026-07-23T07:30:00Z"); + extraData.RootElement.GetProperty("ide").GetString().ShouldBe("IntelliJ IDEA"); + extraData.RootElement.GetProperty("buildKind").GetString().ShouldBe("incremental"); + extraData.RootElement.GetProperty("requestedTasks")[0].GetString().ShouldBe("build"); + extraData.RootElement.GetProperty("taskCount").GetInt32().ShouldBe(42); + extraData.RootElement.GetProperty("executedTaskCount").GetInt32().ShouldBe(14); + extraData.RootElement.GetProperty("upToDateTaskCount").GetInt32().ShouldBe(25); + extraData.RootElement.GetProperty("fromCacheTaskCount").GetInt32().ShouldBe(3); + extraData.RootElement.GetProperty("failedTaskCount").GetInt32().ShouldBe(0); + extraData.RootElement.GetProperty("compileTaskCount").GetInt32().ShouldBe(8); + extraData.RootElement.GetProperty("compileTimeMs").GetInt64().ShouldBe(913); + extraData.RootElement.GetProperty("taskTimeMs").GetInt64().ShouldBe(3276); + extraData.RootElement.GetProperty("projects")[1] + .GetProperty("projectPath").GetString().ShouldBe(":api"); + + var rawPayload = await db.RawPayloads.SingleAsync(); + rawPayload.Endpoint.ShouldBe("/gradle"); + rawPayload.PayloadJson.ShouldContain("\"projects\""); + rawPayload.PayloadJson.ShouldContain("\"compileTimeMs\":533"); + } + + [Test] + public async Task POST_Gradle_WithNullableGitFields_AcceptsPayload() + { + var payload = TestFixtures.CreateGradleBuildMetricPayload( + branch: null, commitSha: null, repository: null, repositoryName: null); + + var response = await TestFixtures.PostJsonAsync(_client, "/gradle", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + await _factory.DrainBackgroundQueuesAsync(); + using var db = _factory.CreateDbContext(); + var metric = await db.BuildMetrics.SingleAsync(); + metric.Branch.ShouldBeEmpty(); + metric.CommitSha.ShouldBeNull(); + metric.Repository.ShouldBeEmpty(); + metric.RepositoryName.ShouldBeEmpty(); + } + + [TestCase("NotGradle", "1842")] + [TestCase("gradle", "1842")] + [TestCase("Gradle", null)] + [TestCase("Gradle", "")] + [TestCase("Gradle", "1.5")] + [TestCase("Gradle", "-1")] + [TestCase("Gradle", "9223372036854775808")] + public async Task POST_Gradle_WithInvalidContract_ReturnsBadRequestWithoutPersisting( + string type, string? timeTaken) + { + var payload = TestFixtures.CreateGradleBuildMetricPayload(type, timeTaken); + + var response = await TestFixtures.PostJsonAsync(_client, "/gradle", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + await _factory.DrainBackgroundQueuesAsync(); + using var db = _factory.CreateDbContext(); + (await db.BuildMetrics.CountAsync()).ShouldBe(0); + (await db.RawPayloads.CountAsync()).ShouldBe(0); + } +} diff --git a/src/Agoda.DevExTelemetry.IntegrationTests/KtorIngestTests.cs b/src/Agoda.DevExTelemetry.IntegrationTests/KtorIngestTests.cs new file mode 100644 index 0000000..30fdbac --- /dev/null +++ b/src/Agoda.DevExTelemetry.IntegrationTests/KtorIngestTests.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace Agoda.DevExTelemetry.IntegrationTests; + +[TestFixture(DatabaseProvider.Sqlite)] +[TestFixture(DatabaseProvider.PostgreSql)] +public class KtorIngestTests +{ + private readonly DatabaseProvider _provider; + private CustomWebApplicationFactory _factory = null!; + private HttpClient _client = null!; + + public KtorIngestTests(DatabaseProvider provider) => _provider = provider; + + [SetUp] + public void SetUp() + { + _factory = new CustomWebApplicationFactory(_provider, enableRawPayloadStorage: true); + _client = _factory.CreateClient(); + } + + [TearDown] + public void TearDown() + { + _client.Dispose(); + _factory.Dispose(); + } + + [TestCase(".KtorStartup")] + [TestCase(".KtorResponse")] + public async Task POST_Ktor_WithSupportedType_QueuesCompleteBuildMetric(string type) + { + var payload = TestFixtures.CreateKtorPayload(type); + var response = await TestFixtures.PostJsonAsync(_client, "/ktor", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + await _factory.DrainBackgroundQueuesAsync(); + + using var db = _factory.CreateDbContext(); + var metric = await db.BuildMetrics.SingleAsync(); + metric.MetricType.ShouldBe(type); + metric.TimeTakenMs.ShouldBe(2431); + metric.BuildCategory.ShouldBe("API"); + metric.ReloadType.ShouldBeNull(); + metric.UserName.ShouldBe("developer"); + metric.CpuCount.ShouldBe(12); + metric.Hostname.ShouldBe("workstation"); + metric.Platform.ShouldBe("Linux"); + metric.Os.ShouldBe("Linux 6.8.0 amd64"); + metric.Branch.ShouldBe("feature/startup-metrics"); + metric.ProjectName.ShouldBe("sample-service"); + metric.Repository.ShouldBe("https://example.com/group/sample-service.git"); + metric.RepositoryName.ShouldBe("sample-service"); + metric.ToolVersion.ShouldBe("0.1.0"); + metric.CommitSha.ShouldBe("6f9c4bd4a2d0f5cfa3fa8e199cc0c630f56a9898"); + metric.IsDebuggerAttached.ShouldBeFalse(); + metric.ExecutionEnvironment.ShouldBe("Local"); + metric.SourceEndpoint.ShouldBe("/ktor"); + + using var extraData = JsonDocument.Parse(metric.ExtraData!); + extraData.RootElement.GetProperty("date").GetString() + .ShouldBe("2026-07-23T07:30:00Z"); + extraData.RootElement.GetProperty("tags").GetProperty("team").GetString() + .ShouldBe("supply"); + + var rawPayload = await db.RawPayloads.SingleAsync(); + rawPayload.Endpoint.ShouldBe("/ktor"); + rawPayload.PayloadJson.ShouldContain($"\"type\":\"{type}\""); + rawPayload.PayloadJson.ShouldContain("\"tags\""); + } + + [Test] + public async Task POST_Ktor_WithNullableGitFieldsAndEmptyTags_AcceptsPayload() + { + var payload = TestFixtures.CreateKtorPayload( + branch: null, + commitSha: null, + repository: null, + repositoryName: null, + tags: new Dictionary()); + + var response = await TestFixtures.PostJsonAsync(_client, "/ktor", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + await _factory.DrainBackgroundQueuesAsync(); + using var db = _factory.CreateDbContext(); + var metric = await db.BuildMetrics.SingleAsync(); + metric.Branch.ShouldBeEmpty(); + metric.CommitSha.ShouldBeNull(); + metric.Repository.ShouldBeEmpty(); + metric.RepositoryName.ShouldBeEmpty(); + + using var extraData = JsonDocument.Parse(metric.ExtraData!); + extraData.RootElement.GetProperty("tags").GetRawText().ShouldBe("{}"); + } + + [TestCase(".AspNetStartup", "2431")] + [TestCase("KtorStartup", "2431")] + [TestCase(".KtorStartup", null)] + [TestCase(".KtorStartup", "")] + [TestCase(".KtorStartup", "invalid")] + [TestCase(".KtorStartup", "NaN")] + [TestCase(".KtorStartup", "Infinity")] + [TestCase(".KtorStartup", "-1")] + public async Task POST_Ktor_WithInvalidContract_ReturnsBadRequestWithoutPersisting( + string type, string? timeTaken) + { + var payload = TestFixtures.CreateKtorPayload(type, timeTaken); + + var response = await TestFixtures.PostJsonAsync(_client, "/ktor", payload); + + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + await _factory.DrainBackgroundQueuesAsync(); + using var db = _factory.CreateDbContext(); + (await db.BuildMetrics.CountAsync()).ShouldBe(0); + (await db.RawPayloads.CountAsync()).ShouldBe(0); + } +} diff --git a/src/Agoda.DevExTelemetry.IntegrationTests/TestFixtures.cs b/src/Agoda.DevExTelemetry.IntegrationTests/TestFixtures.cs index 6829f39..34c7473 100644 --- a/src/Agoda.DevExTelemetry.IntegrationTests/TestFixtures.cs +++ b/src/Agoda.DevExTelemetry.IntegrationTests/TestFixtures.cs @@ -279,6 +279,82 @@ public static object CreateGradlePayload(string? durationMs = "5000", string? cp cacheRatio = "0.75" }; + public static object CreateGradleBuildMetricPayload( + string type = "Gradle", + string? timeTaken = "1842", + string? branch = "feature/build-metrics", + string? commitSha = "6f9c4bd4a2d0f5cfa3fa8e199cc0c630f56a9898", + string? repository = "https://example.com/group/sample-service.git", + string? repositoryName = "sample-service") => + new + { + id = Guid.NewGuid().ToString(), + metricsVersion = "0.1.0", + userName = "developer", + cpuCount = 12, + hostname = "workstation", + platform = "Linux", + os = "Linux 6.8.0 amd64", + timeTaken, + branch, + commitSha, + type, + projectName = "sample-service", + repository, + repositoryName, + date = "2026-07-23T07:30:00Z", + isDebuggerAttached = false, + ide = "IntelliJ IDEA", + buildKind = "incremental", + requestedTasks = new[] { "build" }, + taskCount = 42, + executedTaskCount = 14, + upToDateTaskCount = 25, + fromCacheTaskCount = 3, + failedTaskCount = 0, + compileTaskCount = 8, + compileTimeMs = 913, + taskTimeMs = 3276, + projects = new[] + { + new { projectPath = ":", compileTaskCount = 2, compileTimeMs = 380 }, + new { projectPath = ":api", compileTaskCount = 6, compileTimeMs = 533 } + } + }; + + public static object CreateKtorPayload( + string type = ".KtorStartup", + string? timeTaken = "2431", + string? branch = "feature/startup-metrics", + string? commitSha = "6f9c4bd4a2d0f5cfa3fa8e199cc0c630f56a9898", + string? repository = "https://example.com/group/sample-service.git", + string? repositoryName = "sample-service", + Dictionary? tags = null) => + new + { + id = Guid.NewGuid().ToString(), + metricsVersion = "0.1.0", + userName = "developer", + cpuCount = 12, + hostname = "workstation", + platform = "Linux", + os = "Linux 6.8.0 amd64", + timeTaken, + branch, + commitSha, + type, + projectName = "sample-service", + repository, + repositoryName, + date = "2026-07-23T07:30:00Z", + tags = tags ?? new Dictionary + { + ["team"] = "supply", + ["region"] = "apac" + }, + isDebuggerAttached = false + }; + public static async Task PostJsonAsync(HttpClient client, string url, object payload) { var json = JsonSerializer.Serialize(payload); diff --git a/src/Agoda.DevExTelemetry.WebApi/Controllers/GradleController.cs b/src/Agoda.DevExTelemetry.WebApi/Controllers/GradleController.cs index 104896f..9220f2f 100644 --- a/src/Agoda.DevExTelemetry.WebApi/Controllers/GradleController.cs +++ b/src/Agoda.DevExTelemetry.WebApi/Controllers/GradleController.cs @@ -5,6 +5,8 @@ using Agoda.DevExTelemetry.Core.Models.Entities; using Agoda.DevExTelemetry.Core.Models.Ingest; using Agoda.DevExTelemetry.Core.Services; +using Agoda.DevExTelemetry.WebApi.Services; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Agoda.DevExTelemetry.WebApi.Controllers; @@ -15,15 +17,27 @@ public class GradleController : ControllerBase private readonly IBackgroundTaskQueue _queue; private readonly IEnvironmentDetector _environmentDetector; private readonly IBuildCategoryClassifier _classifier; + private readonly IJvmBuildMetricIngestService _jvmBuildMetricIngestService; public GradleController( IBackgroundTaskQueue queue, IEnvironmentDetector environmentDetector, - IBuildCategoryClassifier classifier) + IBuildCategoryClassifier classifier, + IJvmBuildMetricIngestService jvmBuildMetricIngestService) { _queue = queue; _environmentDetector = environmentDetector; _classifier = classifier; + _jvmBuildMetricIngestService = jvmBuildMetricIngestService; + } + + [HttpPost("gradle")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task IngestBuildMetric([FromBody] GradleBuildMetricPayload payload) + { + var error = await _jvmBuildMetricIngestService.QueueGradleAsync(payload); + return error is null ? Ok() : BadRequest(new { error }); } [HttpPost("gradletalaiot")] diff --git a/src/Agoda.DevExTelemetry.WebApi/Controllers/KtorController.cs b/src/Agoda.DevExTelemetry.WebApi/Controllers/KtorController.cs new file mode 100644 index 0000000..d4f983f --- /dev/null +++ b/src/Agoda.DevExTelemetry.WebApi/Controllers/KtorController.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Agoda.DevExTelemetry.Core.Models.Ingest; +using Agoda.DevExTelemetry.WebApi.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Agoda.DevExTelemetry.WebApi.Controllers; + +[ApiController] +public class KtorController : ControllerBase +{ + private readonly IJvmBuildMetricIngestService _ingestService; + + public KtorController(IJvmBuildMetricIngestService ingestService) + { + _ingestService = ingestService; + } + + [HttpPost("ktor")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Ingest([FromBody] KtorStartupMetricPayload payload) + { + var error = await _ingestService.QueueKtorAsync(payload); + return error is null ? Ok() : BadRequest(new { error }); + } +} diff --git a/src/Agoda.DevExTelemetry.WebApi/Services/IJvmBuildMetricIngestService.cs b/src/Agoda.DevExTelemetry.WebApi/Services/IJvmBuildMetricIngestService.cs new file mode 100644 index 0000000..5e30675 --- /dev/null +++ b/src/Agoda.DevExTelemetry.WebApi/Services/IJvmBuildMetricIngestService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Agoda.DevExTelemetry.Core.Models.Ingest; + +namespace Agoda.DevExTelemetry.WebApi.Services; + +public interface IJvmBuildMetricIngestService +{ + Task QueueGradleAsync(GradleBuildMetricPayload payload); + Task QueueKtorAsync(KtorStartupMetricPayload payload); +} diff --git a/src/Agoda.DevExTelemetry.WebApi/Services/JvmBuildMetricIngestService.cs b/src/Agoda.DevExTelemetry.WebApi/Services/JvmBuildMetricIngestService.cs new file mode 100644 index 0000000..dde1147 --- /dev/null +++ b/src/Agoda.DevExTelemetry.WebApi/Services/JvmBuildMetricIngestService.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Threading.Tasks; +using Agoda.DevExTelemetry.Core.Models.Entities; +using Agoda.DevExTelemetry.Core.Models.Ingest; +using Agoda.DevExTelemetry.Core.Services; +using Agoda.IoC.Core; + +namespace Agoda.DevExTelemetry.WebApi.Services; + +[RegisterPerRequest] +public class JvmBuildMetricIngestService : IJvmBuildMetricIngestService +{ + private readonly IBackgroundTaskQueue _queue; + private readonly IEnvironmentDetector _environmentDetector; + + public JvmBuildMetricIngestService( + IBackgroundTaskQueue queue, + IEnvironmentDetector environmentDetector) + { + _queue = queue; + _environmentDetector = environmentDetector; + } + + public async Task QueueGradleAsync(GradleBuildMetricPayload payload) + { + if (!string.Equals(payload.Type, "Gradle", StringComparison.Ordinal)) + return "Invalid type value"; + + if (!long.TryParse(payload.TimeTaken, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var timeTakenMs) || timeTakenMs < 0) + return "Invalid timeTaken value"; + + var extraData = new Dictionary + { + ["date"] = payload.Date, + ["ide"] = payload.Ide, + ["buildKind"] = payload.BuildKind, + ["requestedTasks"] = payload.RequestedTasks, + ["taskCount"] = payload.TaskCount, + ["executedTaskCount"] = payload.ExecutedTaskCount, + ["upToDateTaskCount"] = payload.UpToDateTaskCount, + ["fromCacheTaskCount"] = payload.FromCacheTaskCount, + ["failedTaskCount"] = payload.FailedTaskCount, + ["compileTaskCount"] = payload.CompileTaskCount, + ["compileTimeMs"] = payload.CompileTimeMs, + ["taskTimeMs"] = payload.TaskTimeMs, + ["projects"] = payload.Projects + }; + + await QueueAsync( + payload, + timeTakenMs, + "Gradle", + "/gradle", + JsonSerializer.Serialize(extraData), + JsonSerializer.Serialize(payload)); + + return null; + } + + public async Task QueueKtorAsync(KtorStartupMetricPayload payload) + { + if (payload.Type is not ".KtorStartup" and not ".KtorResponse") + return "Invalid type value"; + + if (!double.TryParse(payload.TimeTaken, NumberStyles.Float, CultureInfo.InvariantCulture, + out var timeTakenMs) || !double.IsFinite(timeTakenMs) || timeTakenMs < 0) + return "Invalid timeTaken value"; + + var extraData = new Dictionary + { + ["date"] = payload.Date, + ["tags"] = payload.Tags + }; + + await QueueAsync( + payload, + timeTakenMs, + payload.Type, + "/ktor", + JsonSerializer.Serialize(extraData), + JsonSerializer.Serialize(payload)); + + return null; + } + + private async Task QueueAsync( + JvmBuildMetricPayload payload, + double timeTakenMs, + string metricType, + string endpoint, + string extraData, + string rawPayload) + { + var platform = payload.Platform ?? string.Empty; + var environment = _environmentDetector.Detect( + payload.Hostname, payload.IsDebuggerAttached, platform, null); + + var metric = new BuildMetric + { + Id = payload.Id ?? Guid.NewGuid().ToString(), + UserName = payload.UserName ?? string.Empty, + CpuCount = payload.CpuCount, + Hostname = payload.Hostname ?? string.Empty, + Platform = platform, + Os = payload.Os ?? string.Empty, + Branch = payload.Branch ?? string.Empty, + ProjectName = payload.ProjectName ?? string.Empty, + Repository = payload.Repository ?? string.Empty, + RepositoryName = payload.RepositoryName ?? string.Empty, + TimeTakenMs = timeTakenMs, + MetricType = metricType, + BuildCategory = "API", + ReloadType = null, + ToolVersion = payload.MetricsVersion, + CommitSha = payload.CommitSha, + IsDebuggerAttached = payload.IsDebuggerAttached, + ExecutionEnvironment = environment, + SourceEndpoint = endpoint, + ExtraData = extraData + }; + + await _queue.QueueBackgroundWorkItemAsync(new IngestBuildMetricWorkItem + { + BuildMetric = metric, + RawPayloadJson = rawPayload, + RawPayloadEndpoint = endpoint, + RawPayloadContentType = "application/json" + }); + } +}