Skillshub clickup-observability
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/clickup-observability" ~/.claude/skills/comeonoliver-skillshub-clickup-observability && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/clickup-observability/SKILL.mdsource content
ClickUp Observability
Overview
Monitor ClickUp API v2 integrations with metrics (request rate, latency, errors, rate limit usage), distributed tracing, and alerting.
Key Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
| Counter | method, endpoint, status | Total API requests |
| Histogram | method, endpoint | Request latency |
| Counter | status_code, error_type | Errors by type |
| Gauge | token_hash | Rate limit headroom |
| Counter | Times we hit 429 |
Prometheus Instrumentation
import { Registry, Counter, Histogram, Gauge } from 'prom-client'; const registry = new Registry(); const requestCounter = new Counter({ name: 'clickup_requests_total', help: 'Total ClickUp API v2 requests', labelNames: ['method', 'endpoint', 'status'] as const, registers: [registry], }); const requestDuration = new Histogram({ name: 'clickup_request_duration_seconds', help: 'ClickUp API request duration in seconds', labelNames: ['method', 'endpoint'] as const, buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5], registers: [registry], }); const rateLimitGauge = new Gauge({ name: 'clickup_rate_limit_remaining', help: 'ClickUp rate limit remaining requests', registers: [registry], }); const errorCounter = new Counter({ name: 'clickup_errors_total', help: 'ClickUp API errors by status code', labelNames: ['status_code', 'error_type'] as const, registers: [registry], });
Instrumented Client
async function instrumentedClickUpRequest<T>( path: string, options: RequestInit = {} ): Promise<T> { const method = options.method ?? 'GET'; // Normalize endpoint for cardinality control (replace UUIDs) const endpoint = path.replace(/\/[a-zA-Z0-9]{6,}(?=\/|$|\?)/g, '/:id'); const timer = requestDuration.startTimer({ method, endpoint }); try { const response = await fetch(`https://api.clickup.com/api/v2${path}`, { ...options, headers: { 'Authorization': process.env.CLICKUP_API_TOKEN!, 'Content-Type': 'application/json', ...options.headers, }, }); // Update rate limit gauge const remaining = response.headers.get('X-RateLimit-Remaining'); if (remaining) rateLimitGauge.set(parseInt(remaining)); const status = response.ok ? 'success' : `${response.status}`; requestCounter.inc({ method, endpoint, status }); if (!response.ok) { const body = await response.json().catch(() => ({})); errorCounter.inc({ status_code: String(response.status), error_type: body.ECODE ?? 'unknown', }); throw new Error(`ClickUp ${response.status}: ${body.err}`); } return response.json(); } catch (error) { if (!(error instanceof Error && error.message.startsWith('ClickUp'))) { errorCounter.inc({ status_code: 'network', error_type: 'fetch_error' }); } throw error; } finally { timer(); } }
OpenTelemetry Tracing
import { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('clickup-integration', '1.0.0'); async function tracedClickUpCall<T>( operationName: string, path: string, fn: () => Promise<T> ): Promise<T> { return tracer.startActiveSpan(`clickup.${operationName}`, async (span) => { span.setAttribute('clickup.path', path); span.setAttribute('clickup.method', 'GET'); try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error: any) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); span.recordException(error); throw error; } finally { span.end(); } }); }
Structured Logging
import pino from 'pino'; const logger = pino({ name: 'clickup', level: process.env.LOG_LEVEL ?? 'info' }); function logClickUpCall(data: { method: string; path: string; status: number; durationMs: number; rateLimitRemaining?: number; error?: string; }): void { const level = data.status >= 500 ? 'error' : data.status >= 400 ? 'warn' : 'info'; logger[level]({ service: 'clickup', ...data, }, `ClickUp ${data.method} ${data.path} -> ${data.status} (${data.durationMs}ms)`); }
Alert Rules
# prometheus/clickup_alerts.yaml groups: - name: clickup rules: - alert: ClickUpHighErrorRate expr: rate(clickup_errors_total[5m]) / rate(clickup_requests_total[5m]) > 0.05 for: 5m labels: { severity: warning } annotations: summary: "ClickUp API error rate > 5%" - alert: ClickUpHighLatency expr: histogram_quantile(0.95, rate(clickup_request_duration_seconds_bucket[5m])) > 3 for: 5m labels: { severity: warning } annotations: summary: "ClickUp P95 latency > 3s" - alert: ClickUpRateLimitLow expr: clickup_rate_limit_remaining < 10 for: 1m labels: { severity: critical } annotations: summary: "ClickUp rate limit nearly exhausted" - alert: ClickUpAuthFailures expr: increase(clickup_errors_total{status_code="401"}[5m]) > 0 labels: { severity: critical } annotations: summary: "ClickUp authentication failures detected"
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 | Dynamic IDs in labels | Normalize paths (replace IDs with ) |
| Missing metrics | Uninstrumented code path | Wrap all API calls through instrumented client |
| Alert storm | Threshold too sensitive | Tune duration and threshold |
| Trace gaps | Missing context propagation | Ensure span context is passed |
Resources
Next Steps
For incident response, see
clickup-incident-runbook.