Skip to content
Open
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
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,36 +190,47 @@ agentcore project invoke harness \
Use `--target` to select a deployment target. When a project declares exactly
one resource of the requested type, `--name` may be omitted.

### Inspect project Runtime logs
### Inspect project logs

Project logging resolves a logical Runtime name through the selected deployment
target, so the physical Runtime ID and deployment region do not need to be
Project logging resolves a logical resource name through the selected
deployment target, so physical IDs and deployment regions do not need to be
supplied:

```bash
agentcore project log runtime
agentcore project log runtime --name checkout --target production
agentcore project log runtime --name checkout --since 1h --level error
agentcore project log harness
agentcore project log harness --name support --target production
agentcore project log harness --name support --since 1h --level error
```

When the project declares exactly one Runtime, `--name` may be omitted. Use the
imperative `agentcore runtime logs --id <runtimeId>` command when addressing a
Runtime directly or working outside a project.
When the project declares exactly one resource of the requested type, `--name`
may be omitted. For Harnesses, the CLI also resolves the managed Harness to its
underlying Runtime before reading CloudWatch. Use the imperative
`agentcore runtime logs` or `agentcore harness logs` commands when addressing a
physical resource directly or working outside a project.

### Inspect project Runtime traces
### Inspect project traces

Project tracing uses the same logical Runtime and deployment target resolution,
then lists or downloads traces from the resolved Runtime's deployment region:
Project tracing uses the same logical resource and deployment target
resolution, then lists or downloads traces from the resolved Runtime's
deployment region:

```bash
agentcore project traces runtime list
agentcore project traces runtime list --name checkout --target production --since 30m
agentcore project traces runtime get <traceId> --name checkout --output trace.json
agentcore project traces harness list
agentcore project traces harness list --name support --target production --since 30m
agentcore project traces harness get <traceId> --name support --output trace.json
```

When the project declares exactly one Runtime, `--name` may be omitted. Use the
imperative `agentcore runtime traces` commands when addressing a Runtime by
physical ID or working outside a project.
When the project declares exactly one resource of the requested type, `--name`
may be omitted. For Harnesses, the CLI resolves the underlying Runtime before
querying its traces. Use the imperative `agentcore runtime traces` or
`agentcore harness traces` commands when addressing a physical resource
directly or working outside a project.

### Examples

Expand Down Expand Up @@ -298,9 +309,12 @@ agentcore runtime logs --id <runtimeId> --since 2026-08-30T12:00:00Z --until now
agentcore runtime traces list --id <runtimeId> --since 30m
agentcore runtime traces get <traceId> --id <runtimeId> --output trace.json

# Resolve a project Runtime by logical name and deployment target
# Resolve project resources by logical name and deployment target
agentcore project log harness --name support --target production --since 1h
agentcore project traces runtime list --name checkout --target production --since 30m
agentcore project traces runtime get <traceId> --name checkout --output trace.json
agentcore project traces harness list --name support --target production --since 30m
agentcore project traces harness get <traceId> --name support --output trace.json

# Inspect AgentCore Memories without project configuration or deployment
agentcore memory get --id <memoryId>
Expand Down
132 changes: 132 additions & 0 deletions src/handlers/project/log/harness.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { LogSource } from "../../../core/observability/index";
import type { ProjectBackend, ResolveDeployedResourcesBackendInput } from "../../../core/project";
import { ProjectSpecSchema } from "../../../projectSchemas/project";
import {
createSilentLogger,
TestCoreClient,
TestGlobalConfigAccessor,
testIO,
} from "../../../testing";
import { createRootHandler } from "../../index";

const originalCwd = process.cwd();
const temporaryDirectories: string[] = [];
const DEFAULT_TARGET = {
name: "default",
account: "111122223333",
region: "eu-west-1",
} as const;
const HARNESS = { name: "support", path: "app/support" } as const;

afterEach(async () => {
process.chdir(originalCwd);
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});

async function inProject(harnesses: readonly unknown[], targets = [DEFAULT_TARGET]) {
const root = await mkdtemp(join(tmpdir(), "agentcore-project-harness-log-"));
temporaryDirectories.push(root);
await mkdir(join(root, "agentcore"), { recursive: true });
const spec = ProjectSpecSchema.parse({
name: "orders",
version: 1,
harnesses,
});
await writeFile(join(root, "agentcore", "agentcore.json"), JSON.stringify(spec));
await writeFile(join(root, "agentcore", "aws-targets.json"), JSON.stringify(targets));
process.chdir(root);
}

function backend() {
const calls: ResolveDeployedResourcesBackendInput[] = [];
const value: ProjectBackend = {
async *build() {},
async *deploy() {
yield* [];
return { outputs: {} };
},
async resolveDeployedResources(project, input) {
calls.push(input);
return project.spec.harnesses.map(({ name }) => ({
resourceType: "harness" as const,
name,
id: `${name}-AbCdEf1234`,
target: input.target,
}));
},
async resolveProjectResources() {
throw new Error("project Harness logs resolve deployed resources");
},
};
return { calls, value };
}

function command(projectBackend: ProjectBackend) {
const core = new TestCoreClient({ backends: { CDK: projectBackend } });
const io = testIO();
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
globalConfigAccessor: new TestGlobalConfigAccessor(),
});
return {
core,
io,
run: (args: string[] = []) =>
root.route([
"bun",
"agentcore",
"project",
"log",
"harness",
...args,
"--region",
"us-east-1",
]),
};
}

describe("project log harness", () => {
test("resolves the Harness and its managed Runtime before tailing logs", async () => {
await inProject([HARNESS]);
const resolved = backend();
const subject = command(resolved.value);
subject.core.harness.setResolvedRuntime({
runtimeId: "harness_support-XyZ123",
runtimeName: "harness_support",
});
subject.core.observability.logEvents = [
{ timestamp: new Date("2026-09-10T12:00:00Z"), message: "ready" },
];

await subject.run();

expect(resolved.calls).toEqual([{ target: DEFAULT_TARGET }]);
const harnessCall = subject.core.harness.calls[0]!;
expect(harnessCall.method).toBe("resolveRuntime");
expect(harnessCall.args[0]).toBe("support-AbCdEf1234");
expect(harnessCall.args[1]).toEqual({
region: DEFAULT_TARGET.region,
endpointUrl: undefined,
});

const call = subject.core.observability.calls[0]!;
expect(call.method).toBe("tailLogs");
expect(call.args[0] as LogSource).toEqual({
logGroupName: "/aws/bedrock-agentcore/runtimes/harness_support-XyZ123-DEFAULT",
});
expect(call.args[2]).toEqual({ region: DEFAULT_TARGET.region, endpointUrl: undefined });
expect(subject.io.stderr()).toContain(
"Streaming logs for Harness 'support' on target 'default'... (Ctrl+C to stop)",
);
expect(subject.io.stdout()).toBe("2026-09-10T12:00:00.000Z ready");
});
});
55 changes: 55 additions & 0 deletions src/handlers/project/log/harness.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import z from "zod";
import { DEFAULT_ENDPOINT_QUALIFIER, runtimeLogGroup } from "../../../core/observability/index";
import type { AppIO } from "../../../io";
import { DEFAULT_TARGET_NAME } from "../../../projectSchemas/aws-targets";
import { flag, ProjectKey } from "../../../router";
import { createLogsHandler } from "../../observability/logs";
import type { Core } from "../../types";
import { coreOptsFromCtx } from "../../utils";
import { selectProjectResource } from "../selection";

const projectHarnessFlags = [
flag("name", "the logical project Harness name", z.string().optional()),
flag("target", "project deployment target", z.string().min(1).default(DEFAULT_TARGET_NAME)),
flag("qualifier", "the Harness endpoint qualifier", z.string().min(1).optional()),
] as const;

export const createProjectHarnessLogHandler = (core: Core, io: AppIO) =>
createLogsHandler(io, {
name: "harness",
description: "stream or search logs for a Harness in the current project",
flags: projectHarnessFlags,
read: async (ctx, flags, request, signal) => {
const project = ctx.require(ProjectKey);
const name = selectProjectResource(project, "harness", flags.name, "inspect logs for");
const deployed = await core.projectManager.resolveDeployedResource(project, {
target: flags.target,
resourceType: "harness",
name,
});
const options = {
...coreOptsFromCtx(ctx),
region: deployed.target.region,
};
const runtime = await core.harness.resolveRuntime(deployed.id, options, signal);
const source = {
logGroupName: runtimeLogGroup(
runtime.runtimeId,
flags.qualifier ?? DEFAULT_ENDPOINT_QUALIFIER,
),
};

if (request.mode === "search") {
return {
events: core.observability.searchLogs(source, request.query, options, signal),
};
}

return {
events: core.observability.tailLogs(source, request.query, options, signal),
announcement:
`Streaming logs for Harness '${name}' on target '${deployed.target.name}'... ` +
"(Ctrl+C to stop)",
};
},
});
4 changes: 3 additions & 1 deletion src/handlers/project/log/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { AppIO } from "../../../io";
import { withProject } from "../../../middleware";
import { Router } from "../../../router";
import type { Core } from "../../types";
import { createProjectHarnessLogHandler } from "./harness";
import { createProjectRuntimeLogHandler } from "./runtime";

export function createProjectLogHandler(core: Core, io: AppIO): Router {
return new Router("log", "inspect logs for resources in the current project")
.use(withProject({ projectManager: core.projectManager }))
.handler(createProjectRuntimeLogHandler(core, io));
.handler(createProjectRuntimeLogHandler(core, io))
.handler(createProjectHarnessLogHandler(core, io));
}
Loading
Loading