Skip to main content 首页 创作者 jeremylongshore tons-of-skills-marketplace miro-observability
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".
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/jeremylongshore/tons-of-skills-marketplace --skill miro-observability命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 同仓库更多 Skills langchain-deploy-integration Deploy a LangChain 1.0 / LangGraph 1.0 app to Cloud Run, Vercel, or LangServe correctly — with timeouts sized for chain length, cold-start mitigation, SSE anti-buffering headers, and Secret Manager over .env. Use when prepping a first production deploy, debugging a stream that hangs behind a proxy, or diagnosing p99 latency spikes. Trigger with "langchain deploy", "langchain cloud run", "langchain vercel python", "langchain langserve", or "langchain docker".
langchain-langgraph-agents Build a correct LangGraph 1.0 ReAct agent with create_react_agent — typed tools, error propagation, recursion caps, and stop conditions that actually stop. Use when writing a first tool-calling agent, migrating from AgentExecutor or initialize_agent, or diagnosing an agent that loops on vague prompts. Trigger with "langgraph agent", "create_react_agent", "langgraph tool calling", "AgentExecutor migration", or "agent loop cost".
langchain-langgraph-human-in-loop Build LangGraph 1.0 human-in-the-loop approval flows with interrupt_before /
interrupt_after and Command(resume=...) — JSON-serializable state, clean
resume semantics, and UI wiring for approval decisions. Use when adding an
approval gate before an expensive tool call, wiring a Slack/web UI for agent
approvals, or debugging a graph that crashes on interrupt.
Trigger with "langgraph human in loop", "langgraph interrupt_before",
"langgraph approval flow", "Command resume", "langgraph HITL".
jeremylongshore
jeremylongshore/tons-of-skills-marketplace
打开 GitHub 仓库 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.7.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.
Prerequisites
Before applying this guide, confirm you have a Miro app or workspace appropriate to the task, a dedicated non-production board where changes can be tested safely, and only the OAuth scopes or administrative access the procedure requires.
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 ({
: ,
: ,
: [ , , ] ,
: [registry],
});
requestDuration = ({
: ,
: ,
: [ , ] ,
: [ , , , , , , , ],
: [registry],
});
errorCounter = ({
: ,
: ,
: [ , ] ,
: [registry],
});
rateLimitRemaining = ({
: ,
: ,
: [registry],
});
rateLimitUsed = ({
: ,
: ,
: [registry],
});
webhookCounter = ({
: ,
: ,
: [ , ] ,
: [registry],
});
Counter
name
'miro_requests_total'
help
'Total Miro REST API v2 requests'
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 ());
});
Instructions Use the ordered procedures and code samples in this guide as a sequence: begin with the prerequisites, apply the configuration or operational step for the target environment, then perform the documented validation or cleanup before proceeding. Keep credentials in the documented secret store; never hard-code them in source.
Output Following this guide produces the Miro integration outcome for its topic—configuration, validation evidence, operational recovery, or a documented migration result. Record command output and relevant identifiers so a failed step is traceable.
Examples Start with the smallest applicable command or code example in the relevant section, using a dedicated test board and non-production credentials. Confirm the expected response or validation result before applying the pattern to production.
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.