project setup with core files including configuration, package management, and basic structure. Added .gitignore, README, and various TypeScript types for CMS components. Implemented initial components and layouts for the application.

This commit is contained in:
Peter Meier
2025-12-13 23:26:13 +01:00
parent ea288a5bbc
commit b1a556dc6d
167 changed files with 19057 additions and 131 deletions

View File

@@ -0,0 +1,112 @@
# Monitoring & Observability
Der Middlelayer ist mit umfassendem Monitoring ausgestattet:
## 1. Structured Logging (Winston)
**Konfiguration:**
- Log-Level: `LOG_LEVEL` (default: `info`)
- Format: JSON in Production, farbig in Development
- Output: Console + Files (`logs/error.log`, `logs/combined.log`)
**Verwendung:**
```typescript
import { logger, logQuery, logError } from './monitoring/logger.js';
logger.info('Info message', { context: 'data' });
logQuery('GetProducts', { limit: 10 }, 45);
logError(error, { operation: 'getProducts' });
```
## 2. Prometheus Metrics
**Endpoints:**
- `GET http://localhost:9090/metrics` - Prometheus Metrics
- `GET http://localhost:9090/health` - Health Check
**Verfügbare Metriken:**
### Query Metrics
- `graphql_queries_total` - Anzahl der Queries (Labels: `operation`, `status`)
- `graphql_query_duration_seconds` - Query-Dauer (Histogram)
- `graphql_query_complexity` - Query-Komplexität (Gauge)
### Cache Metrics
- `cache_hits_total` - Cache Hits (Label: `cache_type`)
- `cache_misses_total` - Cache Misses (Label: `cache_type`)
### DataService Metrics
- `dataservice_calls_total` - DataService Aufrufe (Labels: `method`, `status`)
- `dataservice_duration_seconds` - DataService Dauer (Histogram)
### Error Metrics
- `errors_total` - Anzahl der Fehler (Labels: `type`, `operation`)
**Beispiel Prometheus Query:**
```promql
# Query Rate
rate(graphql_queries_total[5m])
# Error Rate
rate(errors_total[5m])
# Cache Hit Ratio
rate(cache_hits_total[5m]) / (rate(cache_hits_total[5m]) + rate(cache_misses_total[5m]))
```
## 3. Distributed Tracing
**Features:**
- Automatische Trace-ID-Generierung pro Request
- Span-Tracking für verschachtelte Operationen
- Dauer-Messung für Performance-Analyse
**Trace-IDs werden automatisch in Logs und Metrics eingebunden.**
## Environment Variables
```bash
# Logging
LOG_LEVEL=info # debug, info, warn, error
# Metrics
METRICS_PORT=9090 # Port für Metrics-Endpoint
# Query Complexity
MAX_QUERY_COMPLEXITY=1000 # Max. Query-Komplexität
```
## Integration mit Grafana
**Prometheus Scrape Config:**
```yaml
scrape_configs:
- job_name: 'graphql-middlelayer'
static_configs:
- targets: ['localhost:9090']
```
**Grafana Dashboard:**
- Importiere die Metriken in Grafana
- Erstelle Dashboards für:
- Query Performance
- Cache Hit Rates
- Error Rates
- Request Throughput
## Beispiel-Dashboard Queries
```promql
# Requests pro Sekunde
sum(rate(graphql_queries_total[1m])) by (operation)
# Durchschnittliche Query-Dauer
avg(graphql_query_duration_seconds) by (operation)
# Cache Hit Rate
sum(rate(cache_hits_total[5m])) / (sum(rate(cache_hits_total[5m])) + sum(rate(cache_misses_total[5m])))
# Error Rate
sum(rate(errors_total[5m])) by (type, operation)
```

View File

@@ -0,0 +1,86 @@
import winston from "winston";
/**
* Structured Logging mit Winston
* Erstellt JSON-Logs für bessere Analyse und Monitoring
*/
export 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: "graphql-middlelayer",
environment: process.env.NODE_ENV || "development",
},
transports: [
// Console Output (für Development)
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
const metaStr = Object.keys(meta).length
? JSON.stringify(meta, null, 2)
: "";
return `${timestamp} [${level}]: ${message} ${metaStr}`;
})
),
}),
// File Output (für Production)
...(process.env.NODE_ENV === "production"
? [
new winston.transports.File({
filename: "logs/error.log",
level: "error",
}),
new winston.transports.File({
filename: "logs/combined.log",
}),
]
: []),
],
});
// Helper-Funktionen für strukturiertes Logging
export const logQuery = (
operationName: string,
variables: any,
duration: number
) => {
logger.info("GraphQL Query executed", {
operation: operationName,
variables,
duration: `${duration}ms`,
type: "query",
});
};
export const logError = (error: Error, context?: Record<string, any>) => {
logger.error("Error occurred", {
error: {
message: error.message,
stack: error.stack,
name: error.name,
},
...context,
type: "error",
});
};
export const logCacheHit = (key: string, type: string) => {
logger.debug("Cache hit", {
cacheKey: key,
cacheType: type,
type: "cache",
});
};
export const logCacheMiss = (key: string, type: string) => {
logger.debug("Cache miss", {
cacheKey: key,
cacheType: type,
type: "cache",
});
};

View File

@@ -0,0 +1,90 @@
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
/**
* Prometheus Metrics Registry
* Sammelt Metriken für Monitoring und Alerting
*/
export const register = new Registry();
// Default Metrics (CPU, Memory, etc.)
register.setDefaultLabels({
app: 'graphql-middlelayer',
});
// Query Metrics
export const queryCounter = new Counter({
name: 'graphql_queries_total',
help: 'Total number of GraphQL queries',
labelNames: ['operation', 'status'],
registers: [register],
});
export const queryDuration = new Histogram({
name: 'graphql_query_duration_seconds',
help: 'Duration of GraphQL queries in seconds',
labelNames: ['operation'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [register],
});
// Cache Metrics
export const cacheHits = new Counter({
name: 'cache_hits_total',
help: 'Total number of cache hits',
labelNames: ['cache_type'],
registers: [register],
});
export const cacheMisses = new Counter({
name: 'cache_misses_total',
help: 'Total number of cache misses',
labelNames: ['cache_type'],
registers: [register],
});
// DataService Metrics
export const dataServiceCalls = new Counter({
name: 'dataservice_calls_total',
help: 'Total number of DataService calls',
labelNames: ['method', 'status'],
registers: [register],
});
export const dataServiceDuration = new Histogram({
name: 'dataservice_duration_seconds',
help: 'Duration of DataService calls in seconds',
labelNames: ['method'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
registers: [register],
});
// Error Metrics
export const errorCounter = new Counter({
name: 'errors_total',
help: 'Total number of errors',
labelNames: ['type', 'operation'],
registers: [register],
});
// Active Connections
export const activeConnections = new Gauge({
name: 'active_connections',
help: 'Number of active connections',
registers: [register],
});
// Query Complexity
export const queryComplexityGauge = new Gauge({
name: 'graphql_query_complexity',
help: 'Complexity of GraphQL queries',
labelNames: ['operation'],
registers: [register],
});
/**
* Exportiert Metriken im Prometheus-Format
*/
export async function getMetrics(): Promise<string> {
return register.metrics();
}

View File

@@ -0,0 +1,78 @@
/**
* Einfaches Distributed Tracing
* Erstellt Trace-IDs für Request-Tracking
*/
interface TraceContext {
traceId: string;
spanId: string;
parentSpanId?: string;
startTime: number;
}
const traces = new Map<string, TraceContext>();
/**
* Erstellt einen neuen Trace
*/
export function createTrace(traceId?: string): TraceContext {
const id = traceId || generateTraceId();
const trace: TraceContext = {
traceId: id,
spanId: generateSpanId(),
startTime: Date.now(),
};
traces.set(id, trace);
return trace;
}
/**
* Erstellt einen Child-Span
*/
export function createSpan(traceId: string, parentSpanId?: string): string {
const trace = traces.get(traceId);
if (!trace) {
throw new Error(`Trace ${traceId} not found`);
}
const spanId = generateSpanId();
trace.parentSpanId = parentSpanId || trace.spanId;
return spanId;
}
/**
* Beendet einen Trace und gibt die Dauer zurück
*/
export function endTrace(traceId: string): number {
const trace = traces.get(traceId);
if (!trace) {
return 0;
}
const duration = Date.now() - trace.startTime;
traces.delete(traceId);
return duration;
}
/**
* Generiert eine Trace-ID
*/
function generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Generiert eine Span-ID
*/
function generateSpanId(): string {
return `span-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Holt Trace-Informationen
*/
export function getTrace(traceId: string): TraceContext | undefined {
return traces.get(traceId);
}