Skillsevernote-observability
E

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".

jeremylongshore
1.1k stars
21.9k downloads
Updated 6d ago

Readme

evernote-observability follows the SKILL.md standard. Use the install command to add it to your agent stack.

---
name: evernote-observability
description: |
  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>
---

# 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`.

Install

Requires askill CLI v1.0+

Metadata

LicenseUnknown
Version-
Updated6d ago
Publisherjeremylongshore

Tags

apidatabaseobservabilitysecurity