Agent skill
observability-instrumentation
Add business metrics using AWS EMF (Embedded Metric Format) to API endpoints. Focus on domain-specific metrics only - AWS AppRunner provides default SLO/SLA metrics. Use when implementing new endpoints, adding command handlers, or instrumenting business events.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/observability-instrumentation
SKILL.md
Business Metrics with AWS EMF
Instrument API endpoints with business metrics using AWS CloudWatch Embedded Metric Format (EMF). This skill focuses exclusively on domain-specific metrics - AWS AppRunner already provides infrastructure SLO/SLA metrics automatically.
What This Skill Covers
- Business metrics - Domain events (users created, orders placed, payments processed)
- AWS EMF format - Logs that automatically become CloudWatch metrics
- Event subscribers - Metrics emitted via domain event subscribers (not in handlers)
- Type-safe metrics - Concrete metric classes instead of arrays
- SOLID principles - Single Responsibility (subscribers) + Open/Closed (new metric classes)
What This Skill Does NOT Cover
- Infrastructure metrics - Latency, error rates, RPS (AWS AppRunner provides these)
- SLO/SLA metrics - Availability, response times (AWS AppRunner provides these)
- Distributed tracing - Use AWS X-Ray integration instead
When to Use This Skill
Use this skill when:
- Implementing new API endpoints that have business significance
- Adding domain events that should trigger metric emission
- Tracking domain events for analytics and business intelligence
- Building dashboards for business KPIs
User Service defaults: use
Endpoint=Userwith operations likecreate(registration),update(profile changes), andrequest-password-reset; run checks viamake <command>; keep Domain entities free of framework/validation concerns.
Architecture Overview
Business metrics follow these patterns:
- Metric classes - Each metric type is a concrete class extending
BusinessMetric - Event subscribers - Metrics are emitted via domain event subscribers (not hardcoded in handlers)
- Symfony logger - EMF output goes through Monolog with a custom EMF formatter
- No arrays - All metric configuration uses typed objects, not arrays
- Collections - Multiple metrics use
MetricCollection, not arrays
SOLID Principles in Observability
Single Responsibility Principle (SRP)
Each class has ONE responsibility:
| Class | Responsibility |
|---|---|
UsersCreatedMetric |
Define metric name, value, dimensions |
UserCreatedMetricsSubscriber |
Listen to event, emit metric |
AwsEmfBusinessMetricsEmitter |
Format and write EMF logs |
MetricCollection |
Hold multiple metrics for batch emission |
Anti-pattern: Metrics emitted directly in command handlers (violates SRP - handler should only handle commands)
Open/Closed Principle (OCP)
- Open for extension: Add new metrics via new classes
- Closed for modification: Don't change existing metric/emitter code
// ✅ GOOD: Add new metric by creating new class
final readonly class OrdersPlacedMetric extends EndpointOperationBusinessMetric { ... }
// ❌ BAD: Modify existing emitter to handle new metric type
Why Event Subscribers (Not Handler Injection)
// ❌ BAD: Metrics in command handler (violates SRP)
final class CreateUserHandler
{
public function __construct(
private UserRepository $repository,
private BusinessMetricsEmitterInterface $metrics // Wrong!
) {}
}
// ✅ GOOD: Metrics in dedicated event subscriber
final class UserCreatedMetricsSubscriber implements DomainEventSubscriberInterface
{
public function __invoke(UserCreatedEvent $event): void
{
$this->metricsEmitter->emit($this->metricFactory->create());
}
}
Benefits:
- Handler focuses on domain logic only
- Metrics emission is decoupled and testable
- Easy to add/remove metrics without touching business logic
- Multiple subscribers can react to same event
Type-Safe Metric Class Hierarchy
BusinessMetric (abstract)
├── EndpointOperationBusinessMetric (abstract) - for metrics with Endpoint/Operation dimensions
│ ├── UsersCreatedMetric
│ ├── UsersUpdatedMetric
│ ├── UsersDeletedMetric
│ └── EndpointInvocationsMetric
└── (other base classes for different dimension patterns)
MetricDimensionsInterface
├── EndpointOperationMetricDimensions - Endpoint + Operation
└── (custom dimensions for specific metrics)
MetricDimensions - typed collection of MetricDimension objects
MetricDimension - key/value pair
MetricUnit (enum)
├── COUNT, NONE, SECONDS, MILLISECONDS, BYTES, PERCENT
MetricCollection - typed collection implementing IteratorAggregate, Countable
Why no arrays?
| Arrays | Typed Classes |
|---|---|
| No type safety | Full type checking |
| No IDE autocomplete | IDE support |
| Runtime errors | Compile-time errors |
| Hard to refactor | Easy to refactor |
| No encapsulation | Validation in constructor |
Current Implementation
Metric Base Class (Application Layer)
// src/Shared/Application/Observability/Metric/BusinessMetric.php
abstract readonly class BusinessMetric
{
public function __construct(
private float|int $value,
private MetricUnit $unit
) {}
abstract public function name(): string;
abstract public function dimensions(): MetricDimensionsInterface;
public function value(): float|int { return $this->value; }
public function unit(): MetricUnit { return $this->unit; }
}
Concrete Metric Example
// src/User/Application/Metric/UsersCreatedMetric.php
final readonly class UsersCreatedMetric extends EndpointOperationBusinessMetric
{
private const ENDPOINT = 'User';
private const OPERATION = 'create';
public function __construct(
MetricDimensionsFactoryInterface $dimensionsFactory,
float|int $value = 1
) {
parent::__construct($dimensionsFactory, $value, MetricUnit::COUNT);
}
public function name(): string
{
return 'UsersCreated';
}
protected function endpoint(): string
{
return self::ENDPOINT;
}
protected function operation(): string
{
return self::OPERATION;
}
}
Emitter Interface (Application Layer)
// src/Shared/Application/Observability/Emitter/BusinessMetricsEmitterInterface.php
interface BusinessMetricsEmitterInterface
{
public function emit(BusinessMetric $metric): void;
public function emitCollection(MetricCollection $metrics): void;
}
Metrics Event Subscriber
// src/User/Application/EventSubscriber/UserCreatedMetricsSubscriber.php
/**
* Error handling is automatic via DomainEventMessageHandler in async workers.
* Subscribers stay clean - failures are logged + emit metrics automatically.
* This ensures observability never breaks the main request (AP from CAP theorem).
*/
final readonly class UserCreatedMetricsSubscriber implements DomainEventSubscriberInterface
{
public function __construct(
private BusinessMetricsEmitterInterface $metricsEmitter,
private UsersCreatedMetricFactoryInterface $metricFactory
) {
}
public function __invoke(UserCreatedEvent $event): void
{
$this->metricsEmitter->emit($this->metricFactory->create());
}
public function subscribedTo(): array
{
return [UserCreatedEvent::class];
}
}
AWS EMF Format
AWS Embedded Metric Format allows you to embed custom metrics in structured log events. CloudWatch automatically extracts metrics from EMF-formatted logs.
EMF Log Structure
{
"_aws": {
"Timestamp": 1702425600000,
"CloudWatchMetrics": [
{
"Namespace": "UserService/BusinessMetrics",
"Dimensions": [["Endpoint", "Operation"]],
"Metrics": [{ "Name": "UsersCreated", "Unit": "Count" }]
}
]
},
"Endpoint": "User",
"Operation": "create",
"UsersCreated": 1
}
When this log is written to stdout via the EMF Monolog channel, CloudWatch automatically:
- Extracts
UsersCreatedas a metric - Associates it with the
UserService/BusinessMetricsnamespace - Applies dimensions
EndpointandOperation
Creating New Business Metrics
Step 1: Create the Metric Class
// src/Core/Order/Application/Metric/OrdersPlacedMetric.php
namespace App\Core\Order\Application\Metric;
use App\Shared\Application\Observability\Metric\BusinessMetric;
use App\Shared\Application\Observability\Metric\MetricDimension;
use App\Shared\Application\Observability\Metric\MetricDimensions;
use App\Shared\Application\Observability\Metric\MetricDimensionsFactoryInterface;
use App\Shared\Application\Observability\Metric\MetricDimensionsInterface;
use App\Shared\Application\Observability\Metric\MetricUnit;
final readonly class OrdersPlacedMetricDimensions implements MetricDimensionsInterface
{
public function __construct(
private MetricDimensionsFactoryInterface $dimensionsFactory,
private string $paymentMethod
) {
}
public function values(): MetricDimensions
{
return $this->dimensionsFactory->endpointOperationWith(
'Order',
'create',
new MetricDimension('PaymentMethod', $this->paymentMethod)
);
}
}
final readonly class OrdersPlacedMetric extends BusinessMetric
{
public function __construct(
private MetricDimensionsFactoryInterface $dimensionsFactory,
private string $paymentMethod,
float|int $value = 1
) {
parent::__construct($value, MetricUnit::COUNT);
}
public function name(): string
{
return 'OrdersPlaced';
}
public function dimensions(): MetricDimensionsInterface
{
return new OrdersPlacedMetricDimensions(
dimensionsFactory: $this->dimensionsFactory,
paymentMethod: $this->paymentMethod
);
}
}
Step 2: Create the Event Subscriber
// src/Core/Order/Application/EventSubscriber/OrderPlacedMetricsSubscriber.php
namespace App\Core\Order\Application\EventSubscriber;
use App\Core\Order\Application\Factory\OrdersPlacedMetricFactoryInterface;
use App\Core\Order\Domain\Event\OrderPlacedEvent;
use App\Shared\Application\Observability\Emitter\BusinessMetricsEmitterInterface;
use App\Shared\Domain\Bus\Event\DomainEventSubscriberInterface;
final readonly class OrderPlacedMetricsSubscriber implements DomainEventSubscriberInterface
{
public function __construct(
private BusinessMetricsEmitterInterface $metricsEmitter,
private OrdersPlacedMetricFactoryInterface $metricFactory
) {}
public function __invoke(OrderPlacedEvent $event): void
{
$this->metricsEmitter->emit($this->metricFactory->create($event->paymentMethod()));
}
/**
* @return array<class-string>
*/
public function subscribedTo(): array
{
return [OrderPlacedEvent::class];
}
}
Step 3: For Multiple Metrics - Use MetricCollection
// Emit multiple metrics together (dimensionsFactory injected via constructor)
$this->metricsEmitter->emitCollection(new MetricCollection(
$this->ordersPlacedMetricFactory->create($event->paymentMethod()),
$this->orderValueMetricFactory->create($event->totalAmount())
));
Dimension Best Practices
Recommended Dimensions
| Dimension | Description | Cardinality |
|---|---|---|
Endpoint |
API resource name | Low |
Operation |
CRUD action | Very Low |
PaymentMethod |
Payment type | Low |
UserType |
User segment | Low |
Avoid High-Cardinality Dimensions
Don't use:
- User IDs
- Order IDs
- Session IDs
- Timestamps
These create too many unique metric streams and increase CloudWatch costs.
Metric Naming Conventions
Format
{Entity}{Action} # PascalCase
Examples
| Good | Bad |
|---|---|
UsersCreated |
user_created |
OrdersPlaced |
orders.placed.count |
PaymentsProcessed |
payment-processed |
Guidelines
- Use PascalCase for metric names
- Use plural nouns for counters (UsersCreated not UserCreated)
- Use past tense for completed actions
Testing Business Metrics
Use the Spy in Tests
use App\Shared\Application\Observability\Metric\MetricDimension;
use App\Shared\Infrastructure\Observability\Factory\MetricDimensionsFactory;
use App\Tests\Unit\Shared\Infrastructure\Observability\BusinessMetricsEmitterSpy;
final class UserCreatedMetricsSubscriberTest extends TestCase
{
public function testEmitsMetricOnUserCreated(): void
{
$metricsSpy = new BusinessMetricsEmitterSpy();
$dimensionsFactory = new MetricDimensionsFactory();
$metricFactory = new UsersCreatedMetricFactory($dimensionsFactory);
$logger = $this->createMock(LoggerInterface::class);
$subscriber = new UserCreatedMetricsSubscriber(
$metricsSpy,
$metricFactory,
$logger
);
$event = new UserCreatedEvent($userId, $email);
($subscriber)($event);
self::assertSame(1, $metricsSpy->count());
foreach ($metricsSpy->emitted() as $metric) {
self::assertSame('UsersCreated', $metric->name());
self::assertSame(1, $metric->value());
self::assertSame('User', $metric->dimensions()->values()->get('Endpoint'));
self::assertSame('create', $metric->dimensions()->values()->get('Operation'));
}
// Or use the assertion helper
$metricsSpy->assertEmittedWithDimensions(
'UsersCreated',
new MetricDimension('Endpoint', 'User'),
new MetricDimension('Operation', 'create')
);
}
}
Test Service Configuration
In config/services_test.yaml, the spy is configured:
App\Shared\Application\Observability\Emitter\BusinessMetricsEmitterInterface: '@App\Tests\Unit\Shared\Infrastructure\Observability\BusinessMetricsEmitterSpy'
App\Tests\Unit\Shared\Infrastructure\Observability\BusinessMetricsEmitterSpy:
public: true
CloudWatch Queries
After deploying, query your business metrics:
-- Total endpoint invocations by resource
SELECT SUM(EndpointInvocations)
FROM "UserService/BusinessMetrics"
GROUP BY Endpoint
-- Users created over time
SELECT SUM(UsersCreated)
FROM "UserService/BusinessMetrics"
WHERE Endpoint = 'User'
What NOT to Track
Remember: AWS AppRunner already provides infrastructure metrics.
Don't track:
- Request latency
- Error rates
- Response times
- HTTP status codes
- Memory usage
- CPU usage
Do track:
- Business events (orders placed, users created)
- Business values (order amounts, payment totals)
- Domain-specific actions (logins, uploads, exports)
Success Criteria
After implementing business metrics:
- Each domain event that needs tracking has a corresponding metric subscriber
- Metrics use typed classes (not arrays)
- Metrics are emitted via event subscribers (not hardcoded in handlers)
- Dimensions provide meaningful segmentation
- Unit tests verify metric emission
- No infrastructure metrics (AppRunner handles those)
SOLID Compliance Checklist
- SRP: Each metric class has single purpose (define one metric type)
- SRP: Event subscriber only emits metrics (no business logic)
- OCP: New metrics added via new classes (no modification to emitter)
- OCP: New event subscribers added without changing existing code
- LSP: All metrics properly extend
BusinessMetricbase class - ISP:
MetricDimensionsInterfaceis minimal (onlyvalues()) - DIP: Handlers depend on
EventBusInterface, not concrete metrics
Type Safety Checklist
- NO arrays for metric configuration - use typed classes
- NO arrays for metric collections - use
MetricCollection - All dimensions via
MetricDimensionsInterfaceimplementations - Arrays are allowed only at infrastructure boundaries (JSON serialization, PSR-3 log context)
- Unit enum
MetricUnitused for all units
Files Reference
Metric Classes
src/Shared/Application/Observability/Metric/BusinessMetric.php- Base classsrc/Shared/Application/Observability/Metric/MetricUnit.php- Unit enumsrc/Shared/Application/Observability/Metric/MetricDimension.php- Dimension key/valuesrc/Shared/Application/Observability/Metric/MetricDimensions.php- Dimension collectionsrc/Shared/Application/Observability/Metric/MetricCollection.php- Metrics collectionsrc/Shared/Application/Observability/Metric/EndpointInvocationsMetric.php- Endpoint metricsrc/User/Application/Metric/UsersCreatedMetric.php- User create metricsrc/User/Application/Metric/UsersUpdatedMetric.php- User update metricsrc/User/Application/Metric/UsersDeletedMetric.php- User delete metric
Infrastructure
src/Shared/Application/Observability/Emitter/BusinessMetricsEmitterInterface.php- Interfacesrc/Shared/Infrastructure/Observability/AwsEmfBusinessMetricsEmitter.php- EMF implementationsrc/Shared/Infrastructure/Observability/EmfLogFormatter.php- Monolog formatter
Event Subscribers
src/Shared/Infrastructure/Observability/ApiEndpointBusinessMetricsSubscriber.php- HTTP metricssrc/User/Application/EventSubscriber/UserCreatedMetricsSubscriber.phpsrc/User/Application/EventSubscriber/UserUpdatedMetricsSubscriber.phpsrc/User/Application/EventSubscriber/UserDeletedMetricsSubscriber.php
Configuration
config/packages/monolog.yaml- EMF channel configurationconfig/services.yaml- Production wiringconfig/services_test.yaml- Test spy wiring
AWS Documentation
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?