feat: add observability abstractions and wire to runtime - #2147
feat: add observability abstractions and wire to runtime#2147nborges-aws wants to merge 4 commits into
Conversation
|
Claude Security Review: no high-confidence findings. (run) |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## refactor #2147 +/- ##
============================================
+ Coverage 97.25% 97.28% +0.02%
============================================
Files 508 513 +5
Lines 33902 34090 +188
============================================
+ Hits 32972 33164 +192
+ Misses 930 926 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
Nice, well-factored change. The ObservabilityClient / SourceReader split cleanly separates identity resolution from CloudWatch I/O and leaves an obvious extension point for future resource kinds. A few things I looked at carefully:
- AWS SDK mocking:
sourceReader.test.tsandclient.test.tsmock at the SDK client boundary (send) rather than the pieces of internal state, which matches the guidance in the review criteria. No excessive mocking. - Pagination + limit:
CloudWatchSourceReader.searchLogscorrectly caps the per-pagelimitatquery.limit - yielded, terminates whennextToken === requestToken, and short-circuits onlimit <= 0. Edge cases (limit=1, single page, empty page with token) all look right. - Live Tail:
tailLogshandles both the in-bandSessionTimeoutExceptionevent and the thrown variant, reconnects only when timed out, and exits cleanly on abort. Legacyarn:...:*suffix stripping is guarded and covered. - Missing log group: nice consistent
ResourceNotFoundErrortranslation with actionable message in both search and tail paths (including the pre-flightDescribeLogGroupscase for tail). - Handler wiring: input validation (
--tailvs--since/--until,--limitoutside search mode,--since > --until) all raise typedInputValidationErrors and are covered in tests.withUserCancellationpropagates the abort into the SDK calls. - Telemetry: this codebase currently instruments telemetry at the top-level command run in
src/index.tsrather than per-handler, so no per-feature instrumentation is missing here.
Non-blocking observations if you want to iterate later:
--tailis effectively a no-op flag when neither--sincenor--untilis passed (tailing is already the default in that case). Consider either making search the default with--tailrequired for streaming, or documenting the current behavior in the flag help. Either is fine, just be intentional.ResourceFlagValuesinhandlers/observability/types.tsduplicates the existingFlagsOfinrouter/handler.tsx. Could reuse or export the router one to keep a single source of truth.- The log group naming convention
/aws/bedrock-agentcore/runtimes/<id>-<qualifier>is hard-coded; if the service ever exposes this via an API, worth switching to that to avoid a lurking coupling.
Nothing here blocks merge. Ship it.
AlexanderRichey
left a comment
There was a problem hiding this comment.
There is some good stuff here but I think we can simplify a bit. I like the idea of creating a function that creates these handlers for us, but I think this can be done with a little less abstraction and more directly. It seems like what you want is something like:
const createLogsHandler = (client: ObservabilityClient, io: AppIO) => createHandler(...)Then use this for Runtime and Harness:
export function createRuntimeHandler(...): Router {
...
runtime.handler(createLogsHandler(obsC, io))
...
}| } from "./resolver"; | ||
| import type { LogSearchQuery, LogTailQuery, RawLogRecord, SourceReader } from "./sourceReader"; | ||
|
|
||
| export interface LogRecord { |
There was a problem hiding this comment.
Is this importable from a package somewhere? Is this what all log records look like in AC?
There was a problem hiding this comment.
I looked and couldn't find any good source we could import this from. OpenTelemetry logRecord is the closest thing, but wouldn't support any cloudWatch metadata fields
| logs: readonly LogSource[]; | ||
| } | ||
|
|
||
| export interface ObservabilitySourceResolver<R extends ObservableResourceRef> { |
There was a problem hiding this comment.
Do we really need all of these complex types?
There was a problem hiding this comment.
I was able to simplify a decent chunk of this. The resolver registry is now just a record keyed by the resource type, and got rid of ResolvedResourceIdentity. The logical flow stays the same but we now lookup directly with resolvers[resuorce.kind]. So no mapped Extract type, or generic resolver parameters.
| * Builds reusable logs command behavior. Primitive routers contribute only | ||
| * identity flags and conversion to an ObservableResourceRef. | ||
| */ | ||
| export class ObservabilityHandlerFactory implements ObservabilityHandlerFactories { |
There was a problem hiding this comment.
Interesting. Did you consider writing a function that returns a handler function? That might be simpler.
There was a problem hiding this comment.
Good call. I've refactored the handler as a static method
| const timestamp = Date.parse(trimmed); | ||
| if (!Number.isNaN(timestamp)) return timestamp; | ||
|
|
||
| throw new InputValidationError( |
There was a problem hiding this comment.
Any reason we're not using date-fns?
There was a problem hiding this comment.
No reason except it isn't currently in the project. Using it would save us the ISO parsing, but the rest of the logic would still be needed
| toResource(flags: ResourceFlagValues<F>): Extract<ObservableResourceRef, { kind: K }>; | ||
| } | ||
|
|
||
| export interface ObservabilityHandlerFactories { |
There was a problem hiding this comment.
What's the upshot of an interface for this?
There was a problem hiding this comment.
No reason to keep as interface. I've changed the handlers to come from a static function as suggested
jariy17
left a comment
There was a problem hiding this comment.
I would try to reduce the abstraction by getting rid of the factory. A static function like Alex suggested would reduce the complexity here. Otherwise, this is a very good abstraction that should expand to other AgentCore resources. Thanks for diving deep on this implementation.
| @@ -0,0 +1,144 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
There was a problem hiding this comment.
We should use Golden tests here. Runtime logs handler tests should cover this.
| } from "./resolver"; | ||
| import type { LogSearchQuery, LogTailQuery, RawLogRecord, SourceReader } from "./sourceReader"; | ||
|
|
||
| export interface LogRecord { |
There was a problem hiding this comment.
Since this is ONLY storing data, please declare it as a type.
| * knowledge of Runtime or any other AgentCore resource type. | ||
| */ | ||
| export class CloudWatchSourceReader implements SourceReader { | ||
| constructor(private readonly clients: Pick<AwsClients, "logs">) {} |
There was a problem hiding this comment.
nit: Since we are only using the Logs Client, it should be logClient.
| @@ -0,0 +1,189 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
| import type { LogRecord } from "../../core/observability"; | |||
There was a problem hiding this comment.
Yep golden tests have been added
b5e008a to
c40047e
Compare
|
Claude Security Review: no high-confidence findings. (run) |
Description
This PR adds reusable observability infrastructure. The setup is built to apply generally across our primitives, while allowing for resource-specific customization where necessary. This PR wires runtime to the infrastructure. Remaining primitives wiring will be released as a follow pending alignment on the abstractions added here.
ObservabilityClientas the shared API entry pointlogsunder RuntimeLogRecordArchitecture
Runtime handler → ObservabilityClient → RuntimeSourceResolver → CloudWatchSourceReader → LogRecord
The resolver owns resource-to-log-group translation. The source reader owns CloudWatch mechanics without ever needing knowledge of our resource types. The client is responsible for the orchestration of these layers.
Commands
Tail logs:
Search logs:
--qualifier selects a non-default Runtime endpoint.
Type of Change
Testing
How have you tested the change?
bun run test(2322 pass, 0 fail)npm run test:unitandnpm run test:integnpm run typechecknpm run lintsrc/assets/, I rannpm run test:update-snapshotsand committed the updated snapshotsChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.