evernote-observability
Implement observability for Evernote integrations. Use when setting up monitoring, logging, tracing, or alerting for Evernote applications. Trigger with phrases like "evernote monitoring", "evernote logging", "evernote metrics", "evernote observability". allowed-tools: Read, Write, Edit, Bash(npm:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
evernote-pack
Claude Code skill pack for Evernote (24 skills)
Installation
This skill is included in the evernote-pack plugin:
/plugin install evernote-pack@claude-code-plugins-plus
Click to copy
Instructions
# Evernote Observability
## Overview
Comprehensive observability setup for Evernote integrations including metrics, logging, tracing, and alerting.
## Prerequisites
- Monitoring infrastructure (Prometheus, Datadog, etc.)
- Log aggregation (ELK, CloudWatch, etc.)
- Alerting system
## Instructions
### Step 1: Metrics Collection
```javascript
// monitoring/metrics.js
const prometheus = require('prom-client');
// Initialize default metrics
prometheus.collectDefaultMetrics({ prefix: 'evernote_' });
// API call metrics
const apiCallCounter = new prometheus.Counter({
name: 'evernote_api_calls_total',
help: 'Total number of Evernote API calls',
labelNames: ['operation', 'status', 'sandbox']
});
const apiCallDuration = new prometheus.Histogram({
name: 'evernote_api_call_duration_seconds',
help: 'Duration of Evernote API calls',
labelNames: ['operation'],
buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10]
});
// Rate limit metrics
const rateLimitCounter = new prometheus.Counter({
name: 'evernote_rate_limits_total',
help: 'Total number of rate limit hits'
});
const rateLimitWaitGauge = new prometheus.Gauge({
name: 'evernote_rate_limit_wait_seconds',
help: 'Current rate limit wait time'
});
// Cache metrics
const cacheHitCounter = new prometheus.Counter({
name: 'evernote_cache_hits_total',
help: 'Total cache hits',
labelNames: ['operation']
});
const cacheMissCounter = new prometheus.Counter({
name: 'evernote_cache_misses_total',
help: 'Total cache misses',
labelNames: ['operation']
});
// Auth metrics
const authCounter = new prometheus.Counter({
name: 'evernote_auth_total',
help: 'Total authentication attempts',
labelNames: ['status', 'type']
});
const activeTokensGauge = new prometheus.Gauge({
name: 'evernote_active_tokens',
help: 'Number of active user tokens'
});
// Quota metrics
const quotaUsageGauge = new prometheus.Gauge({
name: 'evernote_quota_usage_bytes',
help: 'Current quota usage in bytes',
labelNames: ['user_id']
});
// Export metrics
module.exports = {
apiCallCounter,
apiCallDuration,
rateLimitCounter,
rateLimitWaitGauge,
cacheHitCounter,
cacheMissCounter,
authCounter,
activeTokensGauge,
quotaUsageGauge,
register: prometheus.register
};
```
### Step 2: Instrumented Client
```javascript
// services/instrumented-client.js
const Evernote = require('evernote');
const metrics = require('../monitoring/metrics');
const logger = require('../logging/logger');
class InstrumentedEvernoteClient {
constructor(accessToken, options = {}) {
this.client = new Evernote.Client({
token: accessToken,
sandbox: options.sandbox || false
});
this.userId = options.userId;
this.sandbox = options.sandbox;
this._noteStore = null;
}
get noteStore() {
if (!this._noteStore) {
this._noteStore = this.wrapStore(
this.client.getNoteStore(),
'NoteStore'
);
}
return this._noteStore;
}
wrapStore(store, storeName) {
const self = this;
return new Proxy(store, {
get(target, prop) {
const original = target[prop];
if (typeof original !== 'function') {
return original;
}
return async (...args) => {
const operation = `${storeName}.${prop}`;
const startTime = Date.now();
// Start timer
const endTimer = metrics.apiCallDuration.startTimer({ operation });
try {
const result = await original.apply(target, args);
// Record success
const duration = (Date.now() - startTime) / 1000;
metrics.apiCallCounter.inc({
operation,
status: 'success',
sandbox: String(self.sandbox)
});
logger.debug('Evernote API call', {
operation,
duration,
userId: self.userId
});
return result;
} catch (error) {
// Record error
metrics.apiCallCounter.inc({
operation,
status: error.errorCode ? `error_${error.errorCode}` : 'error',
sandbox: String(self.sandbox)
});
// Rate limit tracking
if (error.errorCode === 19) {
metrics.rateLimitCounter.inc();
metrics.rateLimitWaitGauge.set(error.rateLimitDuration || 0);
logger.warn('Rate limit hit', {
operation,
userId: self.userId,
waitTime: error.rateLimitDuration
});
} else {
logger.error('Evernote API error', {
operation,
errorCode: error.errorCode,
parameter: error.parameter,
userId: self.userId
});
}
throw error;
} finally {
endTimer();
}
};
}
});
}
}
module.exports = InstrumentedEvernoteClient;
```
### Step 3: Structured Logging
```javascript
// logging/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'evernote-integration',
environment: process.env.NODE_ENV
},
transports: [
new winston.transports.Console({
format: process.env.NODE_ENV === 'development'
? winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
: winston.format.json()
})
]
});
// Add file transport in production
if (process.env.NODE_ENV === 'production') {
logger.add(new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10 * 1024 * 1024,
maxFiles: 5
}));
logger.add(new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10 * 1024 * 1024,
maxFiles: 5
}));
}
// Redact sensitive data
const redactPatterns = [
/S=s\d+:U=[^:]+:[^:]+:[a-f0-9]+/gi, // Evernote tokens
/bearer\s+[^\s]+/gi,
/api[_-]?key[=:]\s*[^\s,}]+/gi
];
function redact(message) {
if (typeof message !== 'string') return message;
let redacted = message;
for (const pattern of redactPatterns) {
redacted = redacted.replace(pattern, '[REDACTED]');
}
return redacted;
}
// Wrap logger methods
const originalLog = logger.log.bind(logger);
logger.log = function(level, message, meta = {}) {
if (typeof message === 'string') {
message = redact(message);
}
if (meta && typeof meta === 'object') {
meta = JSON.parse(redact(JSON.stringify(meta)));
}
return originalLog(level, message, meta);
};
module.exports = logger;
```
### Step 4: Distributed Tracing
```javascript
// tracing/tracer.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { trace, context, SpanKind } = require('@opentelemetry/api');
// Initialize tracer
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'evernote-integration'
})
});
// Configure exporter
if (process.env.JAEGER_ENDPOINT) {
const exporter = new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT
});
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
}
provider.register();
const tracer = trace.getTracer('evernote-integration');
// Traced client wrapper
function traceOperation(operation, fn) {
return async (...args) => {
const span = tracer.startSpan(`evernote.${operation}`, {
kind: SpanKind.CLIENT,
attributes: {
'evernote.operation': operation
}
});
try {
const result = await context.with(
trace.setSpan(context.active(), span),
() => fn(...args)
);
span.setStatus({ code: 0 }); // OK
return result;
} catch (error) {
span.setStatus({
code: 2, // ERROR
message: error.message
});
span.recordException(error);
span.setAttribute('evernote.error_code', error.errorCode);
throw error;
} finally {
span.end();
}
};
}
module.exports = { tracer, traceOperation };
```
### Step 5: Health and Readiness Endpoints
```javascript
// routes/health.js
const express = require('express');
const metrics = require('../monitoring/metrics');
const router = express.Router();
// Liveness probe
router.get('/health/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
// Readiness probe
router.get('/health/ready', async (req, res) => {
const checks = await runHealthChecks();
const allHealthy = checks.every(c => c.status === 'healthy');
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'ready' : 'not_ready',
checks
});
});
// Detailed health status
router.get('/health/detailed', async (req, res) => {
const checks = await runHealthChecks();
res.json({
status: checks.every(c => c.status === 'healthy') ? 'healthy' : 'degraded',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks
});
});
// Prometheus metrics endpoint
router.get('/metrics', async (req, res) => {
res.set('Content-Type', metrics.register.contentType);
res.end(await metrics.register.metrics());
});
async function runHealthChecks() {
const checks = [];
// Database check
try {
await db.query('SELECT 1');
checks.push({ name: 'database', status: 'healthy' });
} catch (error) {
checks.push({ name: 'database', status: 'unhealthy', error: error.message });
}
// Redis check
try {
await redis.ping();
checks.push({ name: 'redis', status: 'healthy' });
} catch (error) {
checks.push({ name: 'redis', status: 'unhealthy', error: error.message });
}
// Memory check
const memUsage = process.memoryUsage();
const heapPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
checks.push({
name: 'memory',
status: heapPercent < 90 ? 'healthy' : 'warning',
heapUsedPercent: heapPercent.toFixed(1)
});
return checks;
}
module.exports = router;
```
### Step 6: Alert Rules
```yaml
# prometheus/alerts.yml
groups:
- name: evernote-alerts
rules:
# High error rate
- alert: EvernoteHighErrorRate
expr: |
sum(rate(evernote_api_calls_total{status=~"error.*"}[5m])) /
sum(rate(evernote_api_calls_total[5m])) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: High Evernote API error rate
description: "Error rate is {{ $value | humanizePercentage }}"
# Rate limiting
- alert: EvernoteRateLimited
expr: rate(evernote_rate_limits_total[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: Evernote rate limit detected
description: "Rate limits are being hit"
# High latency
- alert: EvernoteHighLatency
expr: |
histogram_quantile(0.95, rate(evernote_api_call_duration_seconds_bucket[5m])) > 5
for: 5m
labels:
severity: warning
annotations:
summary: High Evernote API latency
description: "P95 latency is {{ $value }}s"
# Auth failures
- alert: EvernoteAuthFailures
expr: rate(evernote_auth_total{status="failure"}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: High authentication failure rate
description: "Auth failures: {{ $value }} per second"
# Low cache hit rate
- alert: EvernoteLowCacheHitRate
expr: |
sum(rate(evernote_cache_hits_total[5m])) /
(sum(rate(evernote_cache_hits_total[5m])) +
sum(rate(evernote_cache_misses_total[5m]))) < 0.5
for: 15m
labels:
severity: info
annotations:
summary: Low cache hit rate
description: "Cache hit rate is {{ $value | humanizePercentage }}"
```
### Step 7: Grafana Dashboard
```json
{
"dashboard": {
"title": "Evernote Integration",
"panels": [
{
"title": "API Calls Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(evernote_api_calls_total[5m])) by (operation)",
"legendFormat": "{{operation}}"
}
]
},
{
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "sum(rate(evernote_api_calls_total{status=~\"error.*\"}[5m])) / sum(rate(evernote_api_calls_total[5m])) * 100",
"legendFormat": "Error %"
}
]
},
{
"title": "API Latency (P50/P95/P99)",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.5, rate(evernote_api_call_duration_seconds_bucket[5m]))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, rate(evernote_api_call_duration_seconds_bucket[5m]))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, rate(evernote_api_call_duration_seconds_bucket[5m]))",
"legendFormat": "P99"
}
]
},
{
"title": "Rate Limits",
"type": "stat",
"targets": [
{
"expr": "sum(increase(evernote_rate_limits_total[1h]))",
"legendFormat": "Rate Limits (1h)"
}
]
},
{
"title": "Cache Hit Rate",
"type": "gauge",
"targets": [
{
"expr": "sum(rate(evernote_cache_hits_total[5m])) / (sum(rate(evernote_cache_hits_total[5m])) + sum(rate(evernote_cache_misses_total[5m]))) * 100"
}
]
}
]
}
}
```
## Output
- Prometheus metrics collection
- Instrumented Evernote client
- Structured JSON logging
- Distributed tracing with OpenTelemetry
- Health check endpoints
- Prometheus alert rules
- Grafana dashboard configuration
## Key Metrics
| Metric | Type | Purpose |
|--------|------|---------|
| api_calls_total | Counter | Track API usage |
| api_call_duration_seconds | Histogram | Latency monitoring |
| rate_limits_total | Counter | Rate limit tracking |
| cache_hits_total | Counter | Cache effectiveness |
| auth_total | Counter | Auth success/failure |
## Resources
- [Prometheus](https://prometheus.io/docs/)
- [OpenTelemetry](https://opentelemetry.io/docs/)
- [Grafana](https://grafana.com/docs/)
## Next Steps
For incident handling, see `evernote-incident-runbook`.