adding-a-metric by microbus-io
Creates or modify a metric of a microservice. Use when explicitly asked by the user to create or modify a custom metric for a microservice, or when it makes sense to measure a certain operation taken by the microservice.
Content & Writing
169 Stars
5 Forks
Updated Dec 14, 2025, 05:54 PM
Why Use This
This skill provides specialized capabilities for microbus-io's codebase.
Use Cases
- Developing new features in the microbus-io repository
- Refactoring existing code to follow microbus-io standards
- Understanding and working with microbus-io's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
SKILL.md 301 Lines
Total Files 1
Total Size 0 B
License NOASSERTION
---
name: Adding a Metric
description: Creates or modify a metric of a microservice. Use when explicitly asked by the user to create or modify a custom metric for a microservice, or when it makes sense to measure a certain operation taken by the microservice.
---
**CRITICAL**: Do NOT explore or analyze other microservices unless explicitly instructed to do so. The instructions in this skill are self-contained to this microservice.
**CRITICAL**: Do not omit the `MARKER` comments when generating the code. They are intended as waypoints for future edits.
## Workflow
Copy this checklist and track your progress:
```
Creating or modifying a metric:
- [ ] Step 1: Read local AGENTS.md file
- [ ] Step 2: Determine kind
- [ ] Step 3: Determine if observable just in time
- [ ] Step 4: Determine signature
- [ ] Step 5: Determine OpenTelemetry name
- [ ] Step 6: Extend the ToDo interface
- [ ] Step 7: Describe the metric
- [ ] Step 8: Implement the recorders
- [ ] Step 9: Use the metric
- [ ] Step 10: Observe with callback
- [ ] Step 11: Extend the mock
- [ ] Step 12: Test the callback
- [ ] Step 13: Housekeeping
```
#### Step 1: Read Local `AGENTS.md` File
Read the local `AGENTS.md` file in the microservice's directory. It contains microservice-specific instructions that should take precedence over global instructions.
#### Step 2: Determine Kind
Determine if the metric is a:
- **Counter** - an ever increasing number
- **Gauge** - a number that can go up or down
- **Histogram** - a distribution of cumulative values over time
If a histogram, determine the boundaries that determine its buckets.
#### Step 3: Determine If Observable Just in Time
Some metrics, and in particular gauges, it is enough to observe just in time before recording. For example, there's no reason to sample the available system memory constantly if only the last sampling will be reported. Determine if this metric is observable.
#### Step 4: Determine Signature
Determine the Go signature of the metric.
```go
func MyMetric(ctx context.Context, value int, label1 string, label2 string) (err error)
```
Constraints:
- The first input argument must be `ctx context.Context`
- The function must return an `err error`
- The second input argument is the numerical value of the metric and must be of type `int`, `float64` or `time.Duration`
- The remaining input arguments are labels and should be a `string` or any type that can be converted to a string
#### Step 5: Determine OpenTelemetry Name
The OpenTelemetry name of the metric:
- Should have a (single-word) application prefix relevant to the domain the metric belongs to. The prefix is usually the application name itself
- Should have a suffix describing the unit, in plural form
For example, `myapplication_my_metric_units`.
#### Step 6: Extend the `ToDo` interface
Skip this step if the metric is not observable just in time.
Extend the `ToDo` interface in `intermediate.go`.
```go
type ToDo interface {
// ...
OnObserveMyMetric(ctx context.Context) (err error) // MARKER: MyMetric
}
```
#### Step 7: Describe the Metric
Describe the metric in `NewIntermediate` in `intermediate.go`, after the corresponding `HINT` comment.
- Use snake_case for the metric alias
- If the metric has a unit such as `seconds` or `mb`, append it to the alias
- Replace `MyMetric counts X` with the description of the metric
If a histogram:
```go
func NewIntermediate(impl ToDo) *Intermediate {
// ...
svc.DescribeHistogram("myapplication_my_metric_units", "MyMetric counts X", []float64{1, 2, 5, 10, 100}) // MARKER: MyMetric
}
```
If a gauge:
```go
func NewIntermediate(impl ToDo) *Intermediate {
// ...
svc.DescribeGauge("myapplication_my_metric_units", "MyMetric counts X") // MARKER: MyMetric
}
```
If a counter:
```go
func NewIntermediate(impl ToDo) *Intermediate {
// ...
svc.DescribeCounter("myapplication_my_metric_units", "MyMetric counts X") // MARKER: MyMetric
}
```
#### Step 8: Implement the Recorders
Append the recording methods to `intermediate.go`.
- Cast `int` values to `float64`
- Convert `time.Duration` values to seconds using `dur.Seconds()`
- Set an appropriate comment that describes the metric
- Use snake_case for the metric alias and label names
- If the metric has a unit such as `seconds` or `mb`, append it to the alias
If the metric is a histogram, add the following code.
```go
/*
RecordMyMetric records X.
*/
func (svc *Intermediate) RecordMyMetric(ctx context.Context, value int, label1 string, label2 string) (err error) { // MARKER: MyMetric
return svc.RecordHistogram(ctx, "myapplication_my_metric_units", float64(value),
"label_1", utils.AnyToString(label1),
"label_2", utils.AnyToString(label2),
)
}
```
If the metric is a counter, add the following code.
```go
/*
IncrementMyMetric counts X.
*/
func (svc *Intermediate) IncrementMyMetric(ctx context.Context, value int, label1 string, label2 string) (err error) { // MARKER: MyMetric
return svc.IncrementCounter(ctx, "myapplication_my_metric_units", float64(value),
"label_1", utils.AnyToString(label1),
"label_2", utils.AnyToString(label2),
)
}
```
If the metric is a gauge, add the following code.
```go
/*
RecordMyMetric records X.
*/
func (svc *Intermediate) RecordMyMetric(ctx context.Context, value int, label1 string, label2 string) (err error) { // MARKER: MyMetric
return svc.RecordGauge(ctx, "myapplication_my_metric_units", float64(value),
"label_1", utils.AnyToString(label1),
"label_2", utils.AnyToString(label2),
)
}
```
Note that `utils.AnyToString` is defined in `github.com/microbus-io/fabric/utils`.
#### Step 9: Use the Metric
Skip this step if the metric is observable just in time.
Call `IncrementMyMetric` (if counter) or `RecordMyMetric` (if histogram or gauge) from the appropriate location in `service.go` where the metric should be recorded.
```go
svc.IncrementMyMetric(ctx, 1, "label1", "label2")
```
```go
svc.RecordMyMetric(ctx, value, "label1", "label2")
```
#### Step 10: Observe With Callback
Skip this step if the metric is not observable just in time.
Define a callback in `service.go` that calculates the metric and records it using `RecordMyMetric` (if histogram or gauge) or `IncrementMyMetric` (if counter).
```go
func (svc *Service) OnObserveMyMetric(ctx context.Context) (err error) { // MARKER: MyMetric
v, err := calculateMyMetric()
if err != nil {
return errors.Trace(err)
}
err = svc.RecordMyMetric(ctx, v, "label1", "label2")
return errors.Trace(err)
}
```
Call the `OnObserveMyMetric` callback in `doOnObserveMetrics` in `intermediate.go`.
```go
func (svc *Intermediate) doOnObserveMetrics(ctx context.Context) (err error) {
return svc.Parallel(
// ...
func() (err error) { return svc.OnObserveMyMetric(ctx) }, // MARKER: MyMetric
)
}
```
#### Step 11: Extend the Mock
Skip this step if the metric is not observable just in time.
Add a field to the `Mock` structure definition in `mock.go` to hold a mock handler.
```go
type Mock struct {
// ...
mockOnObserveMyMetric func(ctx context.Context) (err error) // MARKER: MyMetric
}
```
Add the stub to the `Mock`.
```go
// MockOnObserveMyMetric sets up a mock handler for OnObserveMyMetric.
func (svc *Mock) MockOnObserveMyMetric(handler func(ctx context.Context) (err error)) *Mock { // MARKER: MyMetric
svc.mockOnObserveMyMetric = handler
return svc
}
// OnObserveMyMetric executes the mock handler.
func (svc *Mock) OnObserveMyMetric(ctx context.Context) (err error) { // MARKER: MyMetric
if svc.mockOnObserveMyMetric == nil {
err = errors.New("mock not implemented", http.StatusNotImplemented)
return
}
err = svc.mockOnObserveMyMetric(ctx)
return errors.Trace(err)
}
```
#### Step 12: Test the Callback
Skip this step if the metric is not observable just in time.
Append the integration test to `service_test.go`.
```go
func TestMyService_OnObserveMyMetric(t *testing.T) { // MARKER: MyMetric
t.Parallel()
ctx := t.Context()
_ = ctx
// Initialize the microservice under test
svc := NewService()
// Run the testing app
app := application.New()
app.Add(
// HINT: Add microservices or mocks required for this test
svc,
)
app.RunInTest(t)
/*
HINT: Use the following pattern for each test case
t.Run("test_case_name", func(t *testing.T) {
assert := testarossa.For(t)
err := svc.OnObserveMyMetric(ctx)
assert.NoError(err)
})
*/
}
```
Skip the remainder of this step if instructed to be "quick" or to skip tests.
Insert test cases at the bottom of the integration test function using the recommended pattern.
```go
t.Run("test_case_name", func(t *testing.T) {
assert := testarossa.For(t)
err := svc.OnObserveMyMetric(ctx)
assert.NoError(err)
})
```
Do not remove the `HINT` comments.
#### Step 13: Housekeeping
Follow the `microbus/housekeeping` skill.
Name Size