Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/generate-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Generate Changelog

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to generate changelog for (e.g., v1.0.0)'
required: false
Comment on lines +8 to +11

jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Generate Changelog
run: node scripts/generate-changelog.js

Comment on lines +27 to +29
- name: Commit Changelog
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add CHANGELOG.md
git commit -m "docs(changelog): update for release [skip ci]" || echo "No changes to commit"
git push origin HEAD:main
45 changes: 45 additions & 0 deletions docs/DESIGN_PATTERNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# TeachLink Design Patterns

This document outlines the standard software design patterns implemented across the TeachLink ecosystem. Utilizing standard design patterns ensures code maintainability, scalability, and consistency for onboarding new contributors.

## 1. Strategy Pattern

**Context**: Conditional branches that change behavior based on a type or flag (e.g., generating explanations for AI recommendations) often result in large, unmaintainable `switch` blocks.
**Usage**: Used in `ExplanationGenerator` (`explainability.ts`) to dynamically select the correct algorithm for extracting dominant signal explanations.

**Example**:
```typescript
export interface ExplanationStrategy {
matches(dominantSignal: string): boolean;
extract(rankingSignal: any, userProfile: Types.UserProfile, similarUsers?: string[]): ExplanationResult;
}

class ContentSignalStrategy implements ExplanationStrategy {
matches(signal: string) { return signal === 'contentSignal'; }
extract(...) { ... }
}
```

## 2. Builder Pattern

**Context**: Classes requiring multiple configuration options and dependencies (especially when many are optional) often lead to confusing constructors (the "telescoping constructor" anti-pattern).
**Usage**: Implemented for `PrivacyComplianceManager` in `privacy.ts` to cleanly instantiate the manager and supply various privacy engines systematically.

**Example**:
```typescript
const privacyManager = new PrivacyComplianceManagerBuilder()
.withAnonymizer(new UserAnonymizer())
.withDifferentialPrivacy(new DifferentialPrivacyEngine(0.5))
.build();
```

## 3. Facade Pattern

**Context**: Complex subsystems with many interconnected mechanisms shouldn't expose their granular logic to the application's business layer.
**Usage**: `PrivacyComplianceManager` acts as a facade, exposing clean, high-level methods like `processUserDataPrivate` while hiding the interactions between `UserAnonymizer`, `DifferentialPrivacyEngine`, and `DataMinimizer`.

## 4. Dependency Injection (DI)

**Context**: Hardcoding dependencies tightly couples classes and makes unit testing incredibly difficult.
**Usage**: Across the NestJS Indexer and system layers, components are injected via class constructors.
**Example**: The `LoggerService` is instantiated by the framework and passed dynamically to processors, allowing the testing environment to swap it for a mock logger without modifying the core service.
58 changes: 58 additions & 0 deletions docs/LOGGING_STRATEGY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# TeachLink Logging Strategy

## Overview

This document defines the standard logging patterns, formats, and levels across the TeachLink ecosystem. Consistent, structured logging is critical for observability, debugging, and security auditing for both off-chain infrastructure (e.g., the Indexer) and on-chain smart contracts.

## 1. Log Levels

The following standard log levels MUST be used across all off-chain services:

- **`error`**: System failures, critical errors requiring immediate attention, uncaught exceptions, and transaction failures.
- **`warn`**: Non-critical errors, anomalous but recoverable states, deprecated API usage, and retries.
- **`info`**: Normal system operations, business logic milestones (e.g., indexer block processing), startup/shutdown events.
- **`debug`**: Detailed diagnostic information, granular state transitions, payload dumps, and tracing for local development.

*Note: The environment variable `LOG_LEVEL` dictates the minimum severity level output by the application.*

## 2. Structured Logging Format

All off-chain services MUST output logs in **Structured JSON format** when running in production (`NODE_ENV=production`). This ensures seamless ingestion by modern log aggregators and monitoring tools (e.g., Datadog, ELK, CloudWatch).

### Standard JSON Schema

- **`timestamp`**: ISO 8601 UTC string (e.g., `2023-11-20T12:00:00.000Z`)
- **`level`**: The log severity level (e.g., `info`, `error`)
- **`context`**: The module, class, or domain emitting the log (e.g., `EventProcessorService`)
- **`message`**: A concise, human-readable description of the event
- **`data`**: (Optional) Additional contextual structured metadata (e.g., `txHash`, `userId`, `contractId`)

### Example Production Log Entry

```json
{
"timestamp": "2023-11-20T12:00:05.123Z",
"level": "info",
"context": "HorizonService",
"message": "Successfully processed bridge transaction",
"data": {
"txHash": "0xabc123...",
"ledger": 456789
}
}
```

In non-production environments, logs may fallback to a human-readable, color-coded console output for better developer experience (DX).

## 3. Smart Contract Logging

Soroban smart contracts operate natively on-chain, where traditional `stdout` logging is not viable for production.

1. **Production Auditing (Events):** Use Soroban `#[contractevent]` structs as the primary mechanism for emitting structured logs. Events inherently provide structured, tamper-evident logging that off-chain indexers consume.
2. **Local Debugging:** Use `env.logs().print(...)` exclusively for local testing and development. These statements should ideally be removed or disabled in production deployments.

## 4. Best Practices

- **Do not log sensitive data:** PII (Personally Identifiable Information), private keys, secrets, and auth tokens MUST NOT be logged.
- **Include correlation IDs:** When processing user requests or multi-step asynchronous tasks, include a correlation identifier in the `data` field to trace the execution path.
- **Keep messages static:** The `message` field should be a static string (e.g., `"User login failed"`), while dynamic variables should be placed in the `data` object to facilitate aggregations and log metrics.
11 changes: 11 additions & 0 deletions docs/logger.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { LoggerService } from './logger.service';

@Global()
@Module({
imports: [ConfigModule],
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
101 changes: 101 additions & 0 deletions docs/logger.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { LoggerService as NestLoggerService, Injectable, Scope } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService implements NestLoggerService {
private context?: string;
private readonly isProduction: boolean;
private readonly logLevel: string;

constructor(private configService: ConfigService) {
this.isProduction = this.configService.get<string>('app.nodeEnv') === 'production';
this.logLevel = this.configService.get<string>('app.logLevel') || 'info';
}

setContext(context: string) {
this.context = context;
}

private shouldLog(level: string): boolean {
const levels = ['debug', 'info', 'warn', 'error'];
const configuredIndex = levels.indexOf(this.logLevel.toLowerCase());
const targetIndex = levels.indexOf(level);
return targetIndex >= configuredIndex;
}
Comment on lines +19 to +24

private formatMessage(level: string, message: any, optionalParams: any[]) {
const timestamp = new Date().toISOString();

let metadata = {};
let currentContext = this.context || 'Application';

if (optionalParams.length > 0) {
// Extract trailing string arguments (usually used as the context internally by NestJS)
if (typeof optionalParams[optionalParams.length - 1] === 'string') {
currentContext = optionalParams.pop();
}

// Next trailing param logic checks if it's an object intended for structured metadata logging
if (optionalParams.length > 0) {
const lastParam = optionalParams[optionalParams.length - 1];
if (typeof lastParam === 'object' && lastParam !== null) {
metadata = optionalParams.pop();
}
}
}

const logEntry = {
timestamp,
level,
context: currentContext,
message: typeof message === 'object' ? JSON.stringify(message) : message,
...(Object.keys(metadata).length > 0 ? { data: metadata } : {}),
};
Comment on lines +47 to +53

if (this.isProduction) {
return JSON.stringify(logEntry);
}

// Human-readable format output tailored for development environments
const colorCode = this.getColorCode(level);
const resetCode = '\x1b[0m';
const metadataStr = Object.keys(metadata).length > 0 ? `\n\x1b[33m[Data]: ${JSON.stringify(metadata, null, 2)}\x1b[0m` : '';

return `${colorCode}[${timestamp}] [${level.toUpperCase()}] [${currentContext}] ${logEntry.message}${resetCode}${metadataStr}`;
}

private getColorCode(level: string): string {
switch (level) {
case 'error': return '\x1b[31m'; // Red
case 'warn': return '\x1b[33m'; // Yellow
case 'info': return '\x1b[32m'; // Green
case 'debug': return '\x1b[36m'; // Cyan
default: return '\x1b[37m'; // White
}
}

log(message: any, ...optionalParams: any[]) {
if (!this.shouldLog('info')) return;
console.log(this.formatMessage('info', message, optionalParams));
}

error(message: any, ...optionalParams: any[]) {
if (!this.shouldLog('error')) return;
console.error(this.formatMessage('error', message, optionalParams));
}

warn(message: any, ...optionalParams: any[]) {
if (!this.shouldLog('warn')) return;
console.warn(this.formatMessage('warn', message, optionalParams));
}

debug(message: any, ...optionalParams: any[]) {
if (!this.shouldLog('debug')) return;
console.debug(this.formatMessage('debug', message, optionalParams));
}

verbose?(message: any, ...optionalParams: any[]) {
if (!this.shouldLog('debug')) return;
console.debug(this.formatMessage('debug', message, optionalParams));
}
}
Loading
Loading