Skip to main content
miro-observability Set up observability for Miro REST API v2 integrations with Prometheus metrics,
OpenTelemetry traces, structured logging, and Grafana dashboards.
Trigger with phrases like "miro monitoring", "miro metrics",
"miro observability", "monitor miro", "miro alerts", "miro tracing".
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/jeremylongshore/claude-code-plugins-plus-skills --skill miro-observabilityThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository Implement user sign-up and sign-in flows with Clerk.
Use when building authentication UI, customizing sign-in experience,
or implementing OAuth social login.
Trigger with phrases like "clerk sign-in", "clerk sign-up",
"clerk login flow", "clerk OAuth", "clerk social login".
Implement session management and middleware with Clerk.
Use when managing user sessions, configuring route protection,
or implementing token refresh and custom JWT templates.
Trigger with phrases like "clerk session", "clerk middleware",
"clerk route protection", "clerk token", "clerk JWT".
Configure enterprise SSO, role-based access control, and organization management.
Use when implementing SSO integration, configuring role-based permissions,
or setting up organization-level controls.
Trigger with phrases like "clerk SSO", "clerk RBAC",
"clerk enterprise", "clerk roles", "clerk permissions", "clerk organizations".
Related occupations SOC
Based on SOC occupation classification
name miro-observability description Set up observability for Miro REST API v2 integrations with Prometheus metrics,
OpenTelemetry traces, structured logging, and Grafana dashboards.
Trigger with phrases like "miro monitoring", "miro metrics",
"miro observability", "monitor miro", "miro alerts", "miro tracing".
allowed-tools Read, Write, Edit version 1.6.0 license MIT author Jeremy Longshore <jeremy@intentsolutions.io> tags ["saas","miro","observability","monitoring"] compatibility Designed for Claude Code
Miro Observability
Overview
Comprehensive monitoring for Miro REST API v2 integrations: Prometheus metrics for request rates and latency, OpenTelemetry traces for request flow, structured logging, and alerting for rate limit and error conditions.
Key Metrics
Metric Type Labels Purpose miro_requests_totalCounter method, endpoint, status Request volume miro_request_duration_secondsHistogram method, endpoint Latency distribution miro_errors_totalCounter error_type, endpoint Error tracking miro_rate_limit_remainingGauge — Credit headroom miro_rate_limit_credits_usedGauge — Credit consumption miro_webhook_events_totalCounter event_type, item_type Webhook volume miro_token_refresh_totalCounter status OAuth health
Prometheus Metrics
import { Registry , Counter , Histogram , Gauge } from 'prom-client' ;
const registry = new Registry ();
registry.setDefaultLabels ({ app : 'miro-integration' });
const requestCounter = new Counter ({
name : 'miro_requests_total' ,
help : 'Total Miro REST API v2 requests' ,
: [ , , ] ,
: [registry],
});
requestDuration = ({
: ,
: ,
: [ , ] ,
: [ , , , , , , , ],
: [registry],
});
errorCounter = ({
: ,
: ,
: [ , ] ,
: [registry],
});
rateLimitRemaining = ({
: ,
: ,
: [registry],
});
rateLimitUsed = ({
: ,
: ,
: [registry],
});
webhookCounter = ({
: ,
: ,
: [ , ] ,
: [registry],
});
labelNames
'method'
'endpoint'
'status'
as
const
registers
const
new
Histogram
name
'miro_request_duration_seconds'
help
'Miro API request latency'
labelNames
'method'
'endpoint'
as
const
buckets
0.05
0.1
0.25
0.5
1
2.5
5
10
registers
const
new
Counter
name
'miro_errors_total'
help
'Miro API errors by type'
labelNames
'error_type'
'endpoint'
as
const
registers
const
new
Gauge
name
'miro_rate_limit_remaining'
help
'Miro rate limit credits remaining'
registers
const
new
Gauge
name
'miro_rate_limit_credits_used'
help
'Miro rate limit credits used in current window'
registers
const
new
Counter
name
'miro_webhook_events_total'
help
'Miro webhook events received'
labelNames
'event_type'
'item_type'
as
const
registers
Instrumented API Client class InstrumentedMiroClient {
async fetch<T>(path : string , method = 'GET' , body ?: unknown ): Promise <T> {
const endpoint = this .normalizeEndpoint (path);
const timer = requestDuration.startTimer ({ method, endpoint });
try {
const response = await fetch (`https://api.miro.com${path} ` , {
method,
headers : {
'Authorization' : `Bearer ${this .token} ` ,
'Content-Type' : 'application/json' ,
},
...(body ? { body : JSON .stringify (body) } : {}),
});
const remaining = response.headers .get ('X-RateLimit-Remaining' );
const limit = response.headers .get ('X-RateLimit-Limit' );
if (remaining) rateLimitRemaining.set (parseInt (remaining));
if (remaining && limit) {
rateLimitUsed.set (parseInt (limit) - parseInt (remaining));
}
requestCounter.inc ({ method, endpoint, status : String (response.status ) });
if (!response.ok ) {
const errorType = response.status === 429 ? 'rate_limit'
: response.status === 401 ? 'auth'
: response.status >= 500 ? 'server'
: 'client' ;
errorCounter.inc ({ error_type : errorType, endpoint });
throw new MiroApiError (response.status , await response.text ());
}
return response.status === 204 ? null as T : await response.json ();
} catch (error) {
if (!(error instanceof MiroApiError )) {
errorCounter.inc ({ error_type : 'network' , endpoint });
}
throw error;
} finally {
timer ();
}
}
private normalizeEndpoint (path : string ): string {
return path
.replace (/\/boards\/[^/]+/ , '/boards/{id}' )
.replace (/\/items\/[^/]+/ , '/items/{id}' )
.replace (/\/sticky_notes\/[^/]+/ , '/sticky_notes/{id}' )
.replace (/\/shapes\/[^/]+/ , '/shapes/{id}' )
.replace (/\/connectors\/[^/]+/ , '/connectors/{id}' )
.replace (/\?.*$/ , '' );
}
}
OpenTelemetry Tracing import { trace, SpanStatusCode , context } from '@opentelemetry/api' ;
const tracer = trace.getTracer ('miro-client' , '1.0.0' );
async function tracedMiroFetch<T>(
path : string ,
method : string ,
body ?: unknown ,
): Promise <T> {
const endpoint = normalizeEndpoint (path);
return tracer.startActiveSpan (`miro.${method} ${endpoint} ` , async (span) => {
span.setAttribute ('miro.method' , method);
span.setAttribute ('miro.endpoint' , endpoint);
span.setAttribute ('miro.api_version' , 'v2' );
try {
const result = await instrumentedClient.fetch <T>(path, method, body);
span.setStatus ({ code : SpanStatusCode .OK });
return result;
} catch (error : any ) {
span.setStatus ({ code : SpanStatusCode .ERROR , message : error.message });
span.setAttribute ('miro.error_status' , error.status ?? 0 );
span.recordException (error);
throw error;
} finally {
span.end ();
}
});
}
Structured Logging import pino from 'pino' ;
const logger = pino ({
name : 'miro-integration' ,
level : process.env .LOG_LEVEL ?? 'info' ,
redact : ['token' , 'accessToken' , 'refreshToken' , 'Authorization' ],
});
function logMiroRequest (method : string , path : string , status : number , durationMs : number ) {
logger.info ({
service : 'miro' ,
event : 'api_request' ,
method,
path : normalizeEndpoint (path),
status,
durationMs : Math .round (durationMs),
rateLimitRemaining : currentRateLimitRemaining,
});
}
function logWebhookEvent (event : MiroBoardEvent ) {
logger.info ({
service : 'miro' ,
event : 'webhook_received' ,
eventType : event.type ,
itemType : event.item .type ,
boardId : event.boardId ,
itemId : event.item .id ,
});
}
Alert Rules (Prometheus AlertManager)
groups:
- name: miro_alerts
rules:
- alert: MiroHighErrorRate
expr: |
rate(miro_errors_total[5m]) /
rate(miro_requests_total[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Miro API error rate > 5%"
dashboard: "https://grafana.myapp.com/d/miro"
- alert: MiroHighLatency
expr: |
histogram_quantile(0.95,
rate(miro_request_duration_seconds_bucket[5m])
) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Miro API P95 latency > 3 seconds"
- alert: MiroRateLimitLow
expr: miro_rate_limit_remaining < 5000
for: 1m
labels:
severity: critical
annotations:
summary: "Miro rate limit credits < 5000 remaining"
runbook: "Reduce request rate immediately. See miro-rate-limits skill."
- alert: MiroAuthFailures
expr: rate(miro_errors_total{error_type="auth"}[5m]) > 0
for: 2m
labels:
severity: critical
annotations:
summary: "Miro authentication failures detected"
runbook: "Check token expiry. Verify OAuth scopes."
- alert: MiroDown
expr: |
sum(rate(miro_requests_total{status=~"5.."}[5m])) /
sum(rate(miro_requests_total[5m])) > 0.5
for: 3m
labels:
severity: critical
annotations:
summary: "Miro API >50% server errors — check status.miro.com"
Grafana Dashboard Panels {
"panels" : [
{
"title" : "Miro Request Rate (req/s)" ,
"targets" : [ { "expr" : "sum(rate(miro_requests_total[1m]))" } ]
} ,
{
"title" : "Miro Latency P50/P95/P99" ,
"targets" : [
{ "expr" : "histogram_quantile(0.50, rate(miro_request_duration_seconds_bucket[5m]))" , "legendFormat" : "P50" } ,
{ "expr" : "histogram_quantile(0.95, rate(miro_request_duration_seconds_bucket[5m]))" , "legendFormat" : "P95" } ,
{ "expr" : "histogram_quantile(0.99, rate(miro_request_duration_seconds_bucket[5m]))" , "legendFormat" : "P99" }
]
} ,
{
"title" : "Rate Limit Credits Remaining" ,
"targets" : [ { "expr" : "miro_rate_limit_remaining" } ]
} ,
{
"title" : "Error Rate by Type" ,
"targets" : [ { "expr" : "sum by(error_type) (rate(miro_errors_total[5m]))" } ]
} ,
{
"title" : "Webhook Events by Type" ,
"targets" : [ { "expr" : "sum by(event_type, item_type) (rate(miro_webhook_events_total[5m]))" } ]
}
]
}
Metrics Endpoint app.get ('/metrics' , async (req, res) => {
res.set ('Content-Type' , registry.contentType );
res.send (await registry.metrics ());
});
Error Handling Issue Cause Solution High cardinality metrics Board/item IDs in labels Normalize endpoint paths Missing traces No context propagation Check OpenTelemetry SDK init Token in logs Inadequate redaction Use pino redact option Alert storms Thresholds too sensitive Increase for duration
Resources
Next Steps For incident response, see miro-incident-runbook.