Skip to content
Merged
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
66 changes: 66 additions & 0 deletions src/Aura.Api/Docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,13 +431,79 @@ Configure file logging in `appsettings.Production.json`:
}
```

## Deployment Configuration

### Server Start Time

The server start time is automatically initialized when the application starts. This timestamp is used by the `/health` endpoint to indicate when the server was last restarted, which is useful for:

- Verifying successful deployments
- Tracking uptime
- Diagnosing issues related to server restarts

**No configuration required** - the server start time is captured automatically at application startup.

### Deployment Tag

The `DEPLOY_TAG` environment variable identifies the specific version or build of the running server. This is useful for:

- Verifying that the correct version is deployed
- Correlating server behavior with specific builds
- Tracking deployments across environments

**Setting the deployment tag**:

**Windows (PowerShell)**:
```powershell
$env:DEPLOY_TAG = "v1.3.1-abc1234"
```

**Windows (System-wide)**:
```powershell
[System.Environment]::SetEnvironmentVariable("DEPLOY_TAG", "v1.3.1-abc1234", "Machine")
```

**Linux/macOS**:
```bash
export DEPLOY_TAG="v1.3.1-abc1234"
```

**Docker/Container**:
```dockerfile
ENV DEPLOY_TAG=v1.3.1-abc1234
```

**Windows Service**:

When deploying as a Windows Service, set the environment variable at the system level before installing the service:

```powershell
[System.Environment]::SetEnvironmentVariable("DEPLOY_TAG", "v1.3.1-abc1234", "Machine")
.\scripts\Update-LocalInstall.ps1
```

If not set, the deployment tag will default to `"unknown"`.

**Health endpoint response**:

The `/health` endpoint includes both the server start time and deployment tag:

```json
{
"status": "healthy",
"startedAt": "2026-02-07T09:12:51Z",
"deployTag": "v1.3.1-abc1234"
}
```

## Environment Variables

### Summary Table

| Variable | Purpose | Example |
|----------|---------|---------|
| `ASPNETCORE_ENVIRONMENT` | Environment name | `Production` |
| `DEPLOY_TAG` | Deployment version identifier | `v1.3.1-abc1234` |
| `AURA_AZUREOPENAI_APIKEY` | Azure OpenAI API key | `<your-api-key>` |
| `AURA_OPENAI_APIKEY` | OpenAI API key | `<your-api-key>` |
| `AURA_DB_HOST` | Database host | `localhost` |
Expand Down
17 changes: 11 additions & 6 deletions src/Aura.Api/Endpoints/HealthEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ public static class HealthEndpoints
/// <summary>
/// Maps all health endpoints to the application.
/// </summary>
public static WebApplication MapHealthEndpoints(this WebApplication app)
/// <param name="app">The web application.</param>
/// <param name="serverStartTime">The server start time.</param>
/// <param name="deploymentTag">The deployment tag.</param>
public static WebApplication MapHealthEndpoints(
this WebApplication app,
DateTime serverStartTime,
string deploymentTag)
{
app.MapGet("/health", GetHealth);
app.MapGet("/health", () => GetHealth(serverStartTime, deploymentTag));
app.MapGet("/health/db", GetDatabaseHealth);
app.MapGet("/health/rag", GetRagHealth);
app.MapGet("/health/ollama", GetLlmHealth);
Expand All @@ -30,12 +36,11 @@ public static WebApplication MapHealthEndpoints(this WebApplication app)
return app;
}

private static object GetHealth() => new
private static object GetHealth(DateTime serverStartTime, string deploymentTag) => new
{
status = "healthy",
healthy = true,
version = "0.1.0",
timestamp = DateTime.UtcNow
startedAt = serverStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture),
deployTag = deploymentTag
};

private static async Task<IResult> GetDatabaseHealth(AuraDbContext db)
Expand Down
6 changes: 5 additions & 1 deletion src/Aura.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
using Aura.Module.Researcher;
using Microsoft.EntityFrameworkCore;

// Server metadata for health endpoint
var ServerStartTime = DateTime.UtcNow;
var DeploymentTag = Environment.GetEnvironmentVariable("DEPLOY_TAG") ?? "unknown";

var builder = WebApplication.CreateBuilder(args);

// Configure as Windows Service when installed as service
Expand Down Expand Up @@ -161,7 +165,7 @@
app.UseGitHubToken();

// Map all endpoint groups
app.MapHealthEndpoints();
app.MapHealthEndpoints(ServerStartTime, DeploymentTag);
app.MapMcpEndpoints();
app.MapAgentEndpoints();
app.MapRagEndpoints();
Expand Down
121 changes: 121 additions & 0 deletions tests/Aura.Api.Tests/Endpoints/HealthEndpointsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// <copyright file="HealthEndpointsTests.cs" company="Aura">
// Copyright (c) Aura. All rights reserved.
// </copyright>

namespace Aura.Api.Tests.Endpoints;

using Aura.Api.Endpoints;
using FluentAssertions;
using Xunit;

public class HealthEndpointsTests
{
[Fact]
public void GetHealth_ReturnsCorrectJsonStructure()
{
// Arrange
var serverStartTime = new DateTime(2026, 2, 7, 9, 12, 51, DateTimeKind.Utc);
var deploymentTag = "v1.3.1-abc1234";

// Act
var result = InvokeGetHealth(serverStartTime, deploymentTag);

// Assert
result.Should().NotBeNull();
var status = GetPropertyValue(result, "status");
var startedAt = GetPropertyValue(result, "startedAt");
var deployTag = GetPropertyValue(result, "deployTag");

status.Should().Be("healthy");
startedAt.Should().Be("2026-02-07T09:12:51Z");
deployTag.Should().Be("v1.3.1-abc1234");
}

[Fact]
public void GetHealth_WithDifferentServerStartTime_ReturnsCorrectTimestamp()
{
// Arrange
var serverStartTime = new DateTime(2025, 12, 25, 14, 30, 0, DateTimeKind.Utc);
var deploymentTag = "v2.0.0";

// Act
var result = InvokeGetHealth(serverStartTime, deploymentTag);

// Assert
var startedAt = GetPropertyValue(result, "startedAt");
startedAt.Should().Be("2025-12-25T14:30:00Z");
}

[Fact]
public void GetHealth_WithEmptyDeployTag_ReturnsEmptyString()
{
// Arrange
var serverStartTime = DateTime.UtcNow;
var deploymentTag = string.Empty;

// Act
var result = InvokeGetHealth(serverStartTime, deploymentTag);

// Assert
var deployTag = GetPropertyValue(result, "deployTag");
deployTag.Should().Be(string.Empty);
}

[Fact]
public void GetHealth_StatusAlwaysHealthy()
{
// Arrange
var serverStartTime = DateTime.UtcNow;
var deploymentTag = "test-tag";

// Act
var result = InvokeGetHealth(serverStartTime, deploymentTag);

// Assert
var status = GetPropertyValue(result, "status");
status.Should().Be("healthy");
}

[Fact]
public void GetHealth_TimestampFormattedAsISO8601()
{
// Arrange
var serverStartTime = new DateTime(2026, 1, 15, 8, 45, 33, DateTimeKind.Utc);
var deploymentTag = "v1.0.0";

// Act
var result = InvokeGetHealth(serverStartTime, deploymentTag);

// Assert
var startedAt = GetPropertyValue(result, "startedAt");
var timestampString = startedAt.ToString();
timestampString.Should().MatchRegex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$");
}

private static object InvokeGetHealth(DateTime serverStartTime, string deploymentTag)
{
var method = typeof(HealthEndpoints).GetMethod(
"GetHealth",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

if (method == null)
{
throw new InvalidOperationException("GetHealth method not found");
}

var result = method.Invoke(null, new object[] { serverStartTime, deploymentTag });
return result ?? throw new InvalidOperationException("GetHealth returned null");
}

private static object GetPropertyValue(object obj, string propertyName)
{
var type = obj.GetType();
var property = type.GetProperty(propertyName);
if (property == null)
{
throw new InvalidOperationException($"Property {propertyName} not found");
}

return property.GetValue(obj) ?? throw new InvalidOperationException($"Property {propertyName} is null");
}
}
Loading