add after to docker logs query

This commit is contained in:
Alex Holliday committed 2026-09-04 12:06:17 -07:00
1 parent 37e1005479
commit 6e0afe223e
7 files changed
+60 -11

No files matched your search

@@ -205,7 +205,7 @@ class MonitorController implements IMonitorController {
const validatedQuery = getDockerContainerLogsQueryValidation.parse(req.query);
const { monitorId, containerName } = validatedParams;
const { before, limit } = validatedQuery;
const { before, after, limit } = validatedQuery;
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getDockerContainerLogs({
@@ -213,6 +213,7 @@ class MonitorController implements IMonitorController {
monitorId,
containerName,
before: before ? new Date(before) : undefined,
after: after ? new Date(after) : undefined,
limit,
});
+15 -5
View File
@@ -333,11 +333,21 @@ export const getDockerContainerNameParamValidation = z.object({
});
export const getDockerContainerByNameQueryValidation = z.object({ dateRange: z.enum(DateRanges).optional() });
export const getDockerContainerLogsQueryValidation = z.object({
before: z.iso.datetime().optional(),
limit: z.coerce.number().int().min(1).max(DOCKER_LOG_PAGE_MAX).default(DOCKER_LOG_PAGE_DEFAULT),
});
export const getDockerContainerLogsQueryValidation = z
.object({
before: z.iso.datetime().optional(),
after: z.iso.datetime().optional(),
limit: z.coerce.number().int().min(1).max(DOCKER_LOG_PAGE_MAX).default(DOCKER_LOG_PAGE_DEFAULT),
})
.superRefine((query, ctx) => {
if (query.before && query.after) {
ctx.addIssue({
code: "custom",
message: "Specify either before or after, not both",
path: ["after"],
});
}
});
// Canonical monitor shape returned by /monitors endpoints. Keep aligned with
// what the controllers actually serialize.
export const monitorResponseSchema = z
@@ -3,7 +3,7 @@ import { DockerLog } from "@/domain/docker/docker-log.type.js";
export interface IDockerLogsRepository {
createDockerLogs(logs: Omit<DockerLog, "id" | "createdAt" | "updatedAt">[]): Promise<number>;
findLastLineTimestamp(monitorId: string, containerId: string): Promise<string | null>;
findByContainerName(args: { monitorId: string; containerName: string; before?: Date; limit: number }): Promise<DockerLog[]>;
findByContainerName(args: { monitorId: string; containerName: string; before?: Date; after?: Date; limit: number }): Promise<DockerLog[]>;
deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>;
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
@@ -64,22 +64,26 @@ class MongoDockerLogsRepository implements IDockerLogsRepository {
monitorId,
containerName,
before,
after,
limit,
}: {
monitorId: string;
containerName: string;
before?: Date;
after?: Date;
limit: number;
}): Promise<DockerLog[]> => {
const docs = await DockerLogModel.find({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.containerName": containerName,
...(before ? { checkedAt: { $lt: before } } : {}),
...(after ? { checkedAt: { $gt: after } } : {}),
})
.sort({ checkedAt: -1 })
.sort({ checkedAt: after ? 1 : -1 })
.limit(limit)
.lean<DockerLogDocument[]>();
return docs.map(this.toEntity);
const entities = docs.map(this.toEntity);
return after ? entities.reverse() : entities;
};
deleteByMonitorId = async (monitorId: string) =>
@@ -80,6 +80,7 @@ export interface IMonitorService {
monitorId: string;
containerName: string;
before?: Date;
after?: Date;
limit: number;
}): Promise<DockerContainerLogsResult>;
getGeoChecksByMonitorId(args: {
@@ -403,12 +404,14 @@ export class MonitorService implements IMonitorService {
monitorId,
containerName,
before,
after,
limit,
}: {
teamId: string;
monitorId: string;
containerName: string;
before?: Date;
after?: Date;
limit: number;
}): Promise<DockerContainerLogsResult> => {
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
@@ -426,8 +429,9 @@ export class MonitorService implements IMonitorService {
});
}
const logs = await this.dockerLogsRepository.findByContainerName({ monitorId, containerName, before, limit });
return { logs, nextCursor: logs.length === limit ? (logs[logs.length - 1]?.checkedAt ?? null) : null };
const logs = await this.dockerLogsRepository.findByContainerName({ monitorId, containerName, before, after, limit });
const nextCursor = !after && logs.length === limit ? (logs[logs.length - 1]?.checkedAt ?? null) : null;
return { logs, nextCursor };
};
getGeoChecksByMonitorId = async ({
@@ -656,6 +656,28 @@ describe("MonitorService", () => {
expect(result.nextCursor).toBeNull();
});
it("passes after through and returns a null cursor even for a full page", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
const dockerLogsRepository = createDockerLogsRepositoryMock();
const after = new Date("2026-01-01T00:00:00.000Z");
const logs = [{ checkedAt: "2026-01-01T02:00:00.000Z" }, { checkedAt: "2026-01-01T01:00:00.000Z" }];
(monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" }));
(dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue(logs);
const { service } = createService({ monitorsRepository, dockerLogsRepository });
const result = await service.getDockerContainerLogs({
teamId: TEAM_ID,
monitorId: MONITOR_ID,
containerName: "web",
after,
limit: 2,
});
expect(dockerLogsRepository.findByContainerName).toHaveBeenCalledWith({ monitorId: MONITOR_ID, containerName: "web", after, limit: 2 });
expect(result.logs).toBe(logs);
expect(result.nextCursor).toBeNull();
});
});
describe("getPageSpeedDetailsById", () => {
@@ -18,6 +18,14 @@ describe("getDockerContainerLogsQueryValidation", () => {
it("rejects a non-ISO before cursor", () => {
expect(() => getDockerContainerLogsQueryValidation.parse({ before: "yesterday" })).toThrow();
});
it("rejects a non-ISO after cursor", () => {
expect(() => getDockerContainerLogsQueryValidation.parse({ after: "yesterday" })).toThrow();
});
it("rejects before and after together", () => {
expect(() => getDockerContainerLogsQueryValidation.parse({ before: "2026-01-02T00:00:00.000Z", after: "2026-01-01T00:00:00.000Z" })).toThrow();
});
});
const baseDnsBody = {