Skip to content

Commit 4039948

Browse files
authored
feat: add EndpointService for last 60-minute for individual endpointdata
2 parents c7c0718 + 9da72e1 commit 4039948

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using ThingConnect.Pulse.Server.Services;
3+
4+
[ApiController]
5+
[Route("api/endpoints")]
6+
public sealed class EndpointsController : ControllerBase
7+
{
8+
private readonly IEndpointService _endpointService;
9+
10+
public EndpointsController(IEndpointService endpointService)
11+
{
12+
_endpointService = endpointService;
13+
}
14+
15+
[HttpGet("{id:guid}")]
16+
public async Task<ActionResult<EndpointDetailDto>> GetEndpointDetail(
17+
Guid id,
18+
[FromQuery] int windowMinutes = 60)
19+
{
20+
var detail = await _endpointService.GetEndpointDetailAsync(id, windowMinutes);
21+
return detail == null ? (ActionResult<EndpointDetailDto>)NotFound() : (ActionResult<EndpointDetailDto>)Ok(detail);
22+
}
23+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using ThingConnect.Pulse.Server.Models;
2+
3+
public sealed class EndpointDetailDto
4+
{
5+
public required EndpointDto Endpoint { get; set; }
6+
public List<RawCheckDto> Recent { get; set; } = [];
7+
public List<OutageDto> Outages { get; set; } = [];
8+
}

ThingConnect.Pulse.Server/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public static async Task Main(string[] args)
155155
builder.Services.AddSingleton<IDiscoveryService, DiscoveryService>();
156156
builder.Services.AddScoped<IStatusService, StatusService>();
157157
builder.Services.AddScoped<IHistoryService, HistoryService>();
158+
builder.Services.AddScoped<IEndpointService, EndpointService>();
158159
builder.Services.AddHostedService<MonitoringBackgroundService>();
159160

160161
// Add rollup services
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using ThingConnect.Pulse.Server.Data;
3+
using ThingConnect.Pulse.Server.Helpers;
4+
using ThingConnect.Pulse.Server.Models;
5+
6+
namespace ThingConnect.Pulse.Server.Services;
7+
8+
public interface IEndpointService
9+
{
10+
Task<EndpointDetailDto?> GetEndpointDetailAsync(Guid id, int windowMinutes = 60);
11+
}
12+
13+
public sealed class EndpointService : IEndpointService
14+
{
15+
private readonly PulseDbContext _context;
16+
private const int RecentFetchLimit = 2000;
17+
18+
public EndpointService(PulseDbContext context)
19+
{
20+
_context = context;
21+
}
22+
23+
public async Task<EndpointDetailDto?> GetEndpointDetailAsync(Guid id, int windowMinutes = 60)
24+
{
25+
// Load endpoint with group
26+
var endpoint = await _context.Endpoints
27+
.Include(e => e.Group)
28+
.FirstOrDefaultAsync(e => e.Id == id);
29+
30+
if (endpoint == null) return null;
31+
32+
var windowStart = DateTimeOffset.UtcNow.AddMinutes(-windowMinutes);
33+
34+
// --- Fetch recent raw checks ---
35+
var rawChecks = await _context.CheckResultsRaw
36+
.Where(c => c.EndpointId == id)
37+
.OrderByDescending(c => c.Ts)
38+
.Take(RecentFetchLimit)
39+
.ToListAsync();
40+
41+
var recent = rawChecks
42+
.Select(c => new RawCheckDto
43+
{
44+
Ts = ConvertToDateTimeOffset(c.Ts),
45+
Status = c.Status.ToString().ToLower(),
46+
RttMs = c.RttMs,
47+
Error = c.Error
48+
})
49+
.Where(r => r.Ts >= windowStart)
50+
.OrderByDescending(r => r.Ts)
51+
.ToList();
52+
53+
// --- Fetch outages within window ---
54+
var outageRaw = await _context.Outages
55+
.Where(o => o.EndpointId == id)
56+
.ToListAsync();
57+
58+
var outages = outageRaw
59+
.Where(o =>
60+
{
61+
var started = ConvertToDateTimeOffset(o.StartedTs);
62+
var ended = o.EndedTs != null ? ConvertToDateTimeOffset(o.EndedTs) : (DateTimeOffset?)null;
63+
return started <= DateTimeOffset.UtcNow && (ended == null || ended >= windowStart);
64+
})
65+
.OrderByDescending(o => ConvertToDateTimeOffset(o.StartedTs))
66+
.Select(o => new OutageDto
67+
{
68+
StartedTs = ConvertToDateTimeOffset(o.StartedTs),
69+
EndedTs = o.EndedTs != null ? ConvertToDateTimeOffset(o.EndedTs) : null,
70+
DurationS = NormalizeDurationToInt(o.DurationSeconds),
71+
LastError = o.LastError
72+
})
73+
.ToList();
74+
75+
// --- Map endpoint DTO ---
76+
var endpointDto = MapToEndpointDto(endpoint);
77+
78+
return new EndpointDetailDto
79+
{
80+
Endpoint = endpointDto,
81+
Recent = recent,
82+
Outages = outages
83+
};
84+
}
85+
86+
private EndpointDto MapToEndpointDto(Data.Endpoint endpoint)
87+
{
88+
return new EndpointDto
89+
{
90+
Id = endpoint.Id,
91+
Name = endpoint.Name,
92+
Group = new GroupDto
93+
{
94+
Id = endpoint.Group.Id,
95+
Name = endpoint.Group.Name,
96+
ParentId = endpoint.Group.ParentId,
97+
Color = endpoint.Group.Color
98+
},
99+
Type = endpoint.Type.ToString().ToLower(),
100+
Host = endpoint.Host,
101+
Port = endpoint.Port,
102+
HttpPath = endpoint.HttpPath,
103+
HttpMatch = endpoint.HttpMatch,
104+
IntervalSeconds = endpoint.IntervalSeconds,
105+
TimeoutMs = endpoint.TimeoutMs,
106+
Retries = endpoint.Retries,
107+
Enabled = endpoint.Enabled
108+
};
109+
}
110+
111+
// --- Helper to convert timestamp to DateTimeOffset ---
112+
private static DateTimeOffset ConvertToDateTimeOffset<T>(T value)
113+
{
114+
if (value is DateTimeOffset dto) return dto;
115+
if (value is DateTime dt) return new DateTimeOffset(dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime());
116+
if (value is long l) return DateTimeOffset.FromUnixTimeSeconds(l);
117+
if (value is int i) return DateTimeOffset.FromUnixTimeSeconds(i);
118+
119+
var s = value?.ToString();
120+
if (!string.IsNullOrEmpty(s) && DateTimeOffset.TryParse(s, out var parsed)) return parsed;
121+
122+
throw new InvalidOperationException($"Unsupported timestamp type: {value?.GetType().FullName ?? "null"}");
123+
}
124+
125+
// --- Helper to normalize duration to int seconds ---
126+
private static int? NormalizeDurationToInt(object? value)
127+
{
128+
if (value == null) return null;
129+
130+
return value switch
131+
{
132+
int i => i,
133+
long l => (int)l,
134+
TimeSpan t => (int)t.TotalSeconds,
135+
double d => (int)Math.Round(d),
136+
float f => (int)Math.Round(f),
137+
_ => int.TryParse(value.ToString(), out var v) ? v : null
138+
};
139+
}
140+
}

0 commit comments

Comments
 (0)