Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement monitoring, logging, and alerting for Linear integrations. Use when setting up metrics collection, dashboards, or configuring alerts for Linear API usage. Trigger: "linear monitoring", "linear observability", "linear metrics", "linear logging", "monitor linear", "linear Prometheus", "linear Grafana".
.claude/skills/jeremylongshore-linear-observability/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -4% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 26% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 27% | 0% |
Production monitoring for Linear integrations using Prometheus metrics, structured logging with pino, health checks, and alerting rules. Track API latency, error rates, rate limit headroom, and webhook throughput.
typescript// src/metrics/linear-metrics.ts import { Counter, Histogram, Gauge, register } from "prom-client"; export const metrics = { // API request tracking apiRequests: new Counter({ name: "linear_api_requests_total", help: "Total Linear API requests", labelNames: ["operation", "status"], }), // Request duration apiLatency: new Histogram({ name: "linear_api_request_duration_seconds", help: "Linear API request duration", labelNames: ["operation"], buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10], }), // Rate limit headroom rateLimitRemaining: new Gauge({ name: "linear_rate_limit_remaining", help: "Remaining rate limit budget", labelNames: ["type"], // "requests" or "complexity" }), // Webhook tracking webhooksReceived: new Counter({ name: "linear_webhooks_received_total", help: "Total webhooks received", labelNames: ["type", "action"], }), webhookProcessingDuration: new Histogram({ name: "linear_webhook_processing_seconds", help: "Webhook processing duration", labelNames: ["type"], buckets: [0.01, 0.05, 0.1, 0.5, 1, 5], }), // Cache effectiveness cacheHits: new Counter({ name: "linear_cache_hits_total", help: "Cache hit count", labelNames: ["key"], }), cacheMisses: new Counter({ name: "linear_cache_misses_total", help: "Cache miss count", labelNames: ["key"], }), }; // Expose metrics endpoint app.get("/metrics", async (req, res) => { res.set("Content-Type", register.contentType); res.end(await register.metrics()); });
typescriptimport { LinearClient } from "@linear/sdk"; function instrumentedCall<T>( operation: string, fn: () => Promise<T> ): Promise<T> { const timer = metrics.apiLatency.startTimer({ operation }); return fn() .then((result) => { metrics.apiRequests.inc({ operation, status: "success" }); timer(); return result; }) .catch((error: any) => { const status = error.status === 429 ? "rate_limited" : "error"; metrics.apiRequests.inc({ operation, status }); timer(); throw error; }); } // Usage const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! }); const teams = await instrumentedCall("teams", () => client.teams()); const issues = await instrumentedCall("issues", () => client.issues({ first: 50 }) );
typescriptimport pino from "pino"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info", formatters: { level: (label) => ({ level: label }), }, }); const linearLog = logger.child({ component: "linear" }); // Log API calls function logApiCall(operation: string, durationMs: number, success: boolean, meta?: any) { linearLog.info({ event: "api_call", operation, durationMs, success, ...meta, }); } // Log webhook events function logWebhook(type: string, action: string, deliveryId: string, meta?: any) { linearLog.info({ event: "webhook", type, action, deliveryId, ...meta, }); } // Log errors with context function logError(operation: string, error: any) { linearLog.error({ event: "error", operation, errorMessage: error.message, errorStatus: error.status, errorType: error.type, // Never log API keys or tokens }); }
typescriptinterface HealthCheck { status: "healthy" | "degraded" | "unhealthy"; checks: Record<string, { status: string; latencyMs?: number; error?: string; }>; timestamp: string; } async function checkLinearHealth(client: LinearClient): Promise<HealthCheck> { const checks: HealthCheck["checks"] = {}; // Check API connectivity const apiStart = Date.now(); try { const viewer = await client.viewer; checks.linear_api = { status: "healthy", latencyMs: Date.now() - apiStart, }; } catch (error: any) { checks.linear_api = { status: "unhealthy", latencyMs: Date.now() - apiStart, error: error.message, }; } // Check rate limit headroom try { const resp = await fetch("https://api.linear.app/graphql", { method: "POST", headers: { Authorization: process.env.LINEAR_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ query: "{ viewer { id } }" }), }); const remaining = parseInt(resp.headers.get("x-ratelimit-requests-remaining") ?? "5000"); metrics.rateLimitRemaining.set({ type: "requests" }, remaining); checks.rate_limit = { status: remaining > 100 ? "healthy" : "degraded", latencyMs: remaining, }; } catch { checks.rate_limit = { status: "unknown" }; } const overall = Object.values(checks).some(c => c.status === "unhealthy") ? "unhealthy" : Object.values(checks).some(c => c.status === "degraded") ? "degraded" : "healthy"; return { status: overall, checks, timestamp: new Date().toISOString() }; } app.get("/health/linear", async (req, res) => { const health = await checkLinearHealth(client); res.status(health.status === "unhealthy" ? 503 : 200).json(health); });
yaml# prometheus/linear-alerts.yml groups: - name: linear rules: - alert: LinearHighErrorRate expr: | rate(linear_api_requests_total{status="error"}[5m]) / rate(linear_api_requests_total[5m]) > 0.05 for: 5m labels: severity: warning annotations: summary: "Linear API error rate > 5%" - alert: LinearRateLimitLow expr: linear_rate_limit_remaining{type="requests"} < 100 for: 2m labels: severity: critical annotations: summary: "Linear rate limit remaining < 100 requests" - alert: LinearHighLatency expr: | histogram_quantile(0.95, rate(linear_api_request_duration_seconds_bucket[5m])) > 2 for: 5m labels: severity: warning annotations: summary: "Linear API p95 latency > 2 seconds" - alert: LinearWebhookProcessingSlow expr: | histogram_quantile(0.95, rate(linear_webhook_processing_seconds_bucket[5m])) > 5 for: 5m labels: severity: warning annotations: summary: "Webhook processing p95 > 5 seconds"
typescript// Instrument webhook handler app.post("/webhooks/linear", express.raw({ type: "*/*" }), async (req, res) => { const start = Date.now(); // ... signature verification ... const event = JSON.parse(req.body.toString()); const delivery = req.headers["linear-delivery"] as string; metrics.webhooksReceived.inc({ type: event.type, action: event.action }); logWebhook(event.type, event.action, delivery); res.json({ ok: true }); try { await processEvent(event); metrics.webhookProcessingDuration.observe( { type: event.type }, (Date.now() - start) / 1000 ); } catch (error: any) { logError("webhook_processing", error); } });
| Error | Cause | Solution | |-------|-------|----------| | Metrics not collecting | Missing instrumentation | Wrap all client calls with instrumentedCall() | | Alerts not firing | Thresholds too high | Adjust based on actual traffic patterns | | Health check timeout | Linear API slow | Add 10s timeout to health check | | Log volume too high | Debug level in production | Set LOG_LEVEL=info in prod |
bashcurl -s http://localhost:3000/health/linear | jq . # { "status": "healthy", "checks": { "linear_api": { "status": "healthy", "latencyMs": 150 } } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 19,458 | 18,577 | -5% | 1 | 1 | 0% | 2,812 | 5,242 | +86% | 0 | 0 | — |
case-06 | pass→pass | 19,641 | 15,116 | -23% | 1 | 1 | 0% | 2,507 | 4,400 | +76% | 0 | 0 | — |
case-01 | fail→pass | 32,090 | 23,291 | -27% | 1 | 1 | 0% | 5,782 | 5,552 | -4% | 0 | 0 | — |
case-02 | fail→pass | 24,267 | 22,517 | -7% | 1 | 1 | 0% | 4,080 | 6,451 | +58% | 0 | 0 | — |
case-03 | fail→fail | 33,714 | 26,777 | -21% | 1 | 1 | 0% | 5,946 | 7,199 | +21% | 0 | 0 | — |
case-04 | pass→pass | 17,037 | 15,193 | -11% | 1 | 1 | 0% | 2,366 | 4,503 | +90% | 0 | 0 | — |
case-07 | fail→fail | 19,946 | 11,647 | -42% | 1 | 1 | 0% | 2,414 | 3,819 | +58% | 0 | 0 | — |
case-08 | fail→pass | 20,706 | 16,527 | -20% | 1 | 1 | 0% | 3,084 | 4,712 | +53% | 0 | 0 | — |
case-09 | fail→fail | 30,495 | 15,066 | -51% | 1 | 1 | 0% | 2,265 | 4,679 | +107% | 0 | 0 | — |
case-10 | fail→fail | 21,021 | 12,475 | -41% | 1 | 1 | 0% | 2,776 | 4,647 | +67% | 0 | 0 | — |
case-11 | pass→pass | 11,462 | 9,265 | -19% | 1 | 1 | 0% | 1,673 | 3,117 | +86% | 0 | 0 | — |
case-12 | pass→pass | 12,769 | 3,940 | -69% | 1 | 1 | 0% | 1,802 | 3,167 | +76% | 0 | 0 | — |
case-13 | fail→pass | 17,936 | 8,088 | -55% | 1 | 1 | 0% | 2,778 | 3,493 | +26% | 0 | 0 | — |
case-14 | fail→pass | 18,946 | 3,846 | -80% | 1 | 1 | 0% | 2,589 | 3,295 | +27% | 0 | 0 | — |
case-15 | pass→pass | 21,280 | 9,677 | -55% | 1 | 1 | 0% | 2,786 | 4,311 | +55% | 0 | 0 | — |
case-20 | fail→pass | 16,616 | 9,389 | -43% | 1 | 1 | 0% | 1,953 | 3,340 | +71% | 0 | 0 | — |
case-16 | fail→pass | 9,268 | 2,650 | -71% | 1 | 1 | 0% | 1,534 | 2,977 | +94% | 0 | 0 | — |
case-17 | fail→fail | 30,130 | 18,168 | -40% | 1 | 1 | 0% | 3,221 | 5,234 | +62% | 0 | 0 | — |
case-18 | pass→pass | 22,323 | 4,272 | -81% | 1 | 1 | 0% | 2,433 | 3,253 | +34% | 0 | 0 | — |
case-19 | fail→pass | 15,675 | 8,639 | -45% | 1 | 1 | 0% | 1,822 | 3,172 | +74% | 0 | 0 | — |
case-21 | fail→pass | 10,127 | 7,456 | -26% | 1 | 1 | 0% | 1,651 | 2,977 | +80% | 0 | 0 | — |
case-22 | pass→pass | 14,958 | 9,231 | -38% | 1 | 1 | 0% | 1,763 | 3,099 | +76% | 0 | 0 | — |
case-23 | pass→pass | 17,719 | 10,665 | -40% | 1 | 1 | 0% | 1,967 | 3,536 | +80% | 0 | 0 | — |
case-24 | pass→pass | 20,206 | 10,302 | -49% | 1 | 1 | 0% | 2,279 | 3,332 | +46% | 0 | 0 | — |
case-25 | fail→fail | 14,974 | 13,153 | -12% | 1 | 1 | 0% | 1,617 | 3,846 | +138% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.