Skip to content

Commit 1df21cc

Browse files
authored
Merge pull request #7 from thiagomvas/feat/dashboard
Feat/dashboard
2 parents 1b1dfbf + d11a6d1 commit 1df21cc

36 files changed

Lines changed: 4639 additions & 41 deletions

docker-compose.dev.yml

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,41 @@
1+
12
version: "3.9"
23

34
services:
45
postgres:
56
image: postgres:15
67
container_name: logport-postgres
7-
restart: unless-stopped
8-
labels:
9-
- com.logport.monitor=true
108
environment:
11-
POSTGRES_USER: ${LOGPORT_POSTGRES_USER}
12-
POSTGRES_PASSWORD: ${LOGPORT_POSTGRES_PASSWORD}
13-
POSTGRES_DB: ${LOGPORT_POSTGRES_DB}
9+
POSTGRES_USER: postgres
10+
POSTGRES_PASSWORD: postgres
11+
POSTGRES_DB: logport
1412
ports:
1513
- "5432:5432"
16-
volumes:
17-
- pgdata:/var/lib/postgresql/data
18-
19-
elasticsearch:
20-
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.1
21-
container_name: logport-elasticsearch
22-
labels:
23-
- com.logport.monitor=true
24-
environment:
25-
- discovery.type=single-node
26-
- ES_JAVA_OPTS=-Xms512m -Xmx512m
27-
- xpack.security.enabled=false
28-
ports:
29-
- "9200:9200"
3014
networks:
3115
- logport-network
3216
volumes:
33-
- esdata:/usr/share/elasticsearch/data
17+
- pgdata:/var/lib/postgresql/data
3418
healthcheck:
35-
test: ["CMD-SHELL", "curl -s http://localhost:9200 || exit 1"]
36-
interval: 10s
19+
test: ["CMD-SHELL", "pg_isready -U postgres"]
20+
interval: 5s
3721
retries: 5
3822

23+
logport:
24+
image: logport
25+
container_name: logport-agent
26+
env_file: .env
27+
depends_on:
28+
postgres:
29+
condition: service_healthy
30+
networks:
31+
- logport-network
32+
ports:
33+
- "8080:8080"
34+
volumes:
35+
- /var/run/docker.sock:/var/run/docker.sock
36+
user: root
3937
networks:
4038
logport-network:
4139

4240
volumes:
4341
pgdata:
44-
esdata:

src/ConsoleApp/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"Timeout while calling external service"
1515
};
1616

17-
using var client = LogPortClient.FromServerUrl("ws://localhost:8080/stream");
17+
using var client = LogPortClient.FromServerUrl("ws://localhost:8080/");
1818
await client.EnsureConnectedAsync();
1919

2020
// Send a log every time a key is pressed

src/LogPort.Agent/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ WORKDIR /app
44
EXPOSE 8080
55
EXPOSE 8081
66

7+
FROM node:20 AS ui-build
8+
WORKDIR /src/LogPort.UI
9+
COPY src/LogPort.UI/package*.json ./
10+
RUN npm ci --verbose
11+
COPY src/LogPort.UI .
12+
RUN npm run build
13+
714
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
815
ARG BUILD_CONFIGURATION=Release
916
WORKDIR /src
@@ -24,4 +31,5 @@ RUN dotnet publish "./LogPort.Agent.csproj" -c $BUILD_CONFIGURATION -o /app/publ
2431
FROM base AS final
2532
WORKDIR /app
2633
COPY --from=publish /app/publish .
27-
ENTRYPOINT ["dotnet", "LogPort.Agent.dll"]
34+
COPY --from=ui-build /src/LogPort.UI/dist ./wwwroot
35+
ENTRYPOINT ["dotnet", "LogPort.Agent.dll"]

src/LogPort.Agent/Endpoints/AnalyticsEndpoints.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ public static class AnalyticsEndpoints
99
{
1010
public static void MapAnalyticsEndpoints(this WebApplication app)
1111
{
12-
app.MapGet("/analytics/histogram", GetHistogram)
12+
app.MapGet("api/analytics/histogram", GetHistogram)
1313
.WithTags("Analytics")
1414
.WithName("GetLogHistogram")
1515
.WithSummary("Retrieves a histogram of log entries over time based on the provided query parameters.");
1616

17-
app.MapGet("/analytics/count-by-type", GetCountByType);
17+
app.MapGet("api/analytics/count-by-type", GetCountByType);
1818
}
1919

2020
private static async Task<IResult> GetHistogram(AnalyticsService service, [AsParameters] LogQueryParameters parameters, [FromQuery] TimeSpan? interval)

src/LogPort.Agent/Endpoints/LogEndpoints.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ public static class LogEndpoints
1313
{
1414
public static void MapLogEndpoints(this WebApplication app)
1515
{
16-
app.MapPost("/logs", AddLogAsync);
16+
app.MapPost("api/logs", AddLogAsync);
1717

18-
app.MapGet("/logs", GetLogsAsync);
18+
app.MapGet("api/logs", GetLogsAsync);
19+
app.MapGet("api/logs/count", CountLogsAsync);
20+
app.MapGet("api/logs/metadata", GetLogMetadataAsync);
1921

2022
MapStreamEndpoint(app);
2123
MapLiveLogsEndpoint(app);
2224
}
2325

2426
private static void MapStreamEndpoint(WebApplication app)
2527
{
26-
app.Map("/stream", async context =>
28+
app.Map("api/stream", async context =>
2729
{
2830
if (!context.WebSockets.IsWebSocketRequest)
2931
{
@@ -69,7 +71,7 @@ await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClos
6971

7072
private static void MapLiveLogsEndpoint(WebApplication app)
7173
{
72-
app.Map("/live-logs", async context =>
74+
app.Map("api/live-logs", async context =>
7375
{
7476
if (!context.WebSockets.IsWebSocketRequest)
7577
{
@@ -102,7 +104,7 @@ await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClos
102104
private static async Task<IResult> AddLogAsync(ILogRepository logRepository, LogEntry log)
103105
{
104106
await logRepository.AddLogAsync(log);
105-
return Results.Created($"/logs", log);
107+
return Results.Created($"api/logs", log);
106108
}
107109

108110
private static async Task<IResult> GetLogsAsync(
@@ -112,4 +114,17 @@ private static async Task<IResult> GetLogsAsync(
112114
var logs = await logRepository.GetLogsAsync(parameters);
113115
return Results.Ok(logs);
114116
}
117+
private static async Task<IResult> CountLogsAsync(
118+
ILogRepository logRepository,
119+
[AsParameters] LogQueryParameters parameters)
120+
{
121+
var count = await logRepository.CountLogsAsync(parameters);
122+
return Results.Ok(new { Count = count });
123+
}
124+
125+
private static async Task<IResult> GetLogMetadataAsync(ILogRepository repository)
126+
{
127+
var metadata = await repository.GetLogMetadataAsync();
128+
return Results.Ok(metadata);
129+
}
115130
}

src/LogPort.Agent/Program.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@
1717
var builder = WebApplication.CreateBuilder(args);
1818

1919
builder.Services.AddOpenApi();
20-
20+
builder.Services.AddCors(options =>
21+
{
22+
options.AddPolicy("AllowAll", policy =>
23+
{
24+
policy
25+
.AllowAnyOrigin()
26+
.AllowAnyMethod()
27+
.AllowAnyHeader();
28+
});
29+
});
2130
builder.Configuration.AddEnvironmentVariables(prefix: "LOGPORT_");
2231
var logPortConfig = LogPortConfig.LoadFromEnvironment();
2332
builder.Configuration.GetSection("LOGPORT").Bind(logPortConfig);
@@ -59,11 +68,14 @@
5968

6069
var app = builder.Build();
6170

71+
app.UseCors("AllowAll");
72+
6273
if (app.Environment.IsDevelopment())
6374
{
6475
app.MapOpenApi();
6576
}
6677

78+
6779
app.MapHealthChecks("/health", new HealthCheckOptions
6880
{
6981
ResponseWriter = async (context, report) =>
@@ -91,6 +103,10 @@
91103
}
92104
});
93105
app.UseWebSockets();
106+
107+
app.UseDefaultFiles();
108+
app.UseStaticFiles();
109+
94110
app.MapLogEndpoints();
95111
app.MapAnalyticsEndpoints();
96112

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace LogPort.Core.Models;
2+
3+
public class LogMetadata
4+
{
5+
public string[] LogLevels { get; set; } = [];
6+
public string[] Environments { get; set; } = [];
7+
public string[] Services { get; set; } = [];
8+
public string[] Hostnames { get; set; } = [];
9+
public Dictionary<string, int> LogCountByLevel { get; set; } = new();
10+
public Dictionary<string, int> LogCountByService { get; set; } = new();
11+
public Dictionary<string, int> LogCountByEnvironment { get; set; } = new();
12+
public Dictionary<string, int> LogCountByHostname { get; set; } = new();
13+
public long LogCount { get; set; }
14+
}

src/LogPort.Internal.Common/Interface/ILogRepository.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public interface ILogRepository
99
Task<IEnumerable<LogEntry>> GetLogsAsync(LogQueryParameters parameters);
1010
IAsyncEnumerable<IReadOnlyList<LogEntry>> GetBatchesAsync(LogQueryParameters parameters, int batchSize);
1111
Task<long> CountLogsAsync(LogQueryParameters parameters);
12+
Task<LogMetadata> GetLogMetadataAsync();
1213
}

src/LogPort.Internal.ElasticSearch/ElasticLogRepository.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ public async Task<long> CountLogsAsync(LogQueryParameters parameters)
105105
return response.Count;
106106
}
107107

108+
public Task<LogMetadata> GetLogMetadataAsync()
109+
{
110+
throw new NotImplementedException();
111+
}
112+
108113
private Func<QueryContainerDescriptor<LogEntry>, QueryContainer> BuildQuery(LogQueryParameters parameters)
109114
{
110115
return q =>

src/LogPort.Internal.Postgres/PostgresLogRepository.cs

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public async Task AddLogsAsync(IEnumerable<LogEntry> logs)
5252
new NpgsqlParameter($"svc{i}", (object?)log.ServiceName ?? DBNull.Value),
5353
new NpgsqlParameter($"lvl{i}", log.Level),
5454
new NpgsqlParameter($"msg{i}", log.Message),
55-
new NpgsqlParameter($"meta{i}", NpgsqlDbType.Jsonb) { Value = JsonSerializer.Serialize(log.Metadata, _jsonOptions) },
55+
new NpgsqlParameter($"meta{i}", NpgsqlDbType.Jsonb)
56+
{ Value = JsonSerializer.Serialize(log.Metadata, _jsonOptions) },
5657
new NpgsqlParameter($"trace{i}", (object?)log.TraceId ?? DBNull.Value),
5758
new NpgsqlParameter($"span{i}", (object?)log.SpanId ?? DBNull.Value),
5859
new NpgsqlParameter($"host{i}", (object?)log.Hostname ?? DBNull.Value),
@@ -61,8 +62,9 @@ public async Task AddLogsAsync(IEnumerable<LogEntry> logs)
6162
i++;
6263
}
6364

64-
var sql = "INSERT INTO logs (timestamp, service_name, level, message, metadata, trace_id, span_id, hostname, environment) VALUES "
65-
+ string.Join(", ", sqlValues);
65+
var sql =
66+
"INSERT INTO logs (timestamp, service_name, level, message, metadata, trace_id, span_id, hostname, environment) VALUES "
67+
+ string.Join(", ", sqlValues);
6668

6769
await using var conn = new NpgsqlConnection(_connectionString);
6870
await conn.OpenAsync();
@@ -108,7 +110,7 @@ public async IAsyncEnumerable<IReadOnlyList<LogEntry>> GetBatchesAsync(
108110
var sqlParams = new List<NpgsqlParameter>();
109111
BuildFilters(sql, sqlParams, parameters);
110112

111-
sql.Append(" ORDER BY timestamp ASC");
113+
sql.Append(" ORDER BY timestamp ASC");
112114

113115
await using var conn = new NpgsqlConnection(_connectionString);
114116
await conn.OpenAsync();
@@ -149,9 +151,84 @@ public async Task<long> CountLogsAsync(LogQueryParameters parameters)
149151
return (long)await cmd.ExecuteScalarAsync();
150152
}
151153

154+
public async Task<LogMetadata> GetLogMetadataAsync()
155+
{
156+
const string sql = @"
157+
WITH
158+
lvl_counts AS (
159+
SELECT jsonb_object_agg(level, count) AS data
160+
FROM (SELECT level, COUNT(*) AS count FROM logs WHERE level IS NOT NULL GROUP BY level) t
161+
),
162+
svc_counts AS (
163+
SELECT jsonb_object_agg(service_name, count) AS data
164+
FROM (SELECT service_name, COUNT(*) AS count FROM logs WHERE service_name IS NOT NULL GROUP BY service_name) t
165+
),
166+
env_counts AS (
167+
SELECT jsonb_object_agg(environment, count) AS data
168+
FROM (SELECT environment, COUNT(*) AS count FROM logs WHERE environment IS NOT NULL GROUP BY environment) t
169+
),
170+
host_counts AS (
171+
SELECT jsonb_object_agg(hostname, count) AS data
172+
FROM (SELECT hostname, COUNT(*) AS count FROM logs WHERE hostname IS NOT NULL GROUP BY hostname) t
173+
),
174+
distincts AS (
175+
SELECT
176+
array_agg(DISTINCT level) AS levels,
177+
array_agg(DISTINCT environment) AS environments,
178+
array_agg(DISTINCT service_name) AS services,
179+
array_agg(DISTINCT hostname) AS hostnames,
180+
COUNT(*) AS log_count
181+
FROM logs
182+
)
183+
SELECT
184+
d.levels,
185+
d.environments,
186+
d.services,
187+
d.hostnames,
188+
d.log_count,
189+
l.data AS log_count_by_level,
190+
s.data AS log_count_by_service,
191+
e.data AS log_count_by_environment,
192+
h.data AS log_count_by_hostname
193+
FROM distincts d, lvl_counts l, svc_counts s, env_counts e, host_counts h;
194+
195+
";
196+
197+
await using var conn = new NpgsqlConnection(_connectionString);
198+
await conn.OpenAsync();
199+
await using var cmd = new NpgsqlCommand(sql, conn);
200+
await using var reader = await cmd.ExecuteReaderAsync();
201+
202+
if (!await reader.ReadAsync())
203+
throw new InvalidOperationException("Failed to read log metadata.");
204+
205+
return new LogMetadata
206+
{
207+
LogLevels = reader.IsDBNull(0) ? [] : reader.GetFieldValue<string[]>(0),
208+
Environments = reader.IsDBNull(1) ? [] : reader.GetFieldValue<string[]>(1),
209+
Services = reader.IsDBNull(2) ? [] : reader.GetFieldValue<string[]>(2),
210+
Hostnames = reader.IsDBNull(3) ? [] : reader.GetFieldValue<string[]>(3),
211+
LogCount = reader.GetInt64(4),
212+
LogCountByLevel = reader.IsDBNull(5)
213+
? new()
214+
: JsonSerializer.Deserialize<Dictionary<string, int>>(reader.GetString(5))!,
215+
LogCountByService = reader.IsDBNull(6)
216+
? new()
217+
: JsonSerializer.Deserialize<Dictionary<string, int>>(reader.GetString(6))!,
218+
LogCountByEnvironment = reader.IsDBNull(7)
219+
? new()
220+
: JsonSerializer.Deserialize<Dictionary<string, int>>(reader.GetString(7))!,
221+
LogCountByHostname = reader.IsDBNull(8)
222+
? new()
223+
: JsonSerializer.Deserialize<Dictionary<string, int>>(reader.GetString(8))!
224+
};
225+
}
226+
227+
152228
private async Task EnsurePartitionAsync(DateTime timestamp)
153229
{
154-
var startDate = timestamp.Date.AddDays(-((timestamp.Date - DateTime.MinValue.Date).Days % _partitionLengthInDays));
230+
var startDate =
231+
timestamp.Date.AddDays(-((timestamp.Date - DateTime.MinValue.Date).Days % _partitionLengthInDays));
155232
var endDate = startDate.AddDays(_partitionLengthInDays);
156233

157234
var partitionName = $"logs_{startDate:yyyy_MM_dd}_{_partitionLengthInDays}d";
@@ -205,6 +282,7 @@ void AddFilter(string column, object? value)
205282
parameters.Add(new NpgsqlParameter($"p{idx}", query.From.Value));
206283
idx++;
207284
}
285+
208286
if (query.To.HasValue)
209287
{
210288
sql.Append($" AND timestamp <= @p{idx}");
@@ -224,6 +302,7 @@ void AddFilter(string column, object? value)
224302
sql.Append($" AND message ILIKE @p{idx}");
225303
parameters.Add(new NpgsqlParameter($"p{idx}", $"%{query.Search}%"));
226304
}
305+
227306
idx++;
228307
}
229308

@@ -256,4 +335,4 @@ private LogEntry MapReader(NpgsqlDataReader reader) =>
256335
Hostname = reader.IsDBNull(7) ? null : reader.GetString(7),
257336
Environment = reader.IsDBNull(8) ? null : reader.GetString(8),
258337
};
259-
}
338+
}

0 commit comments

Comments
 (0)