Skip to content

Instantly share code, notes, and snippets.

@eladcandroid
Created September 18, 2025 14:44
Show Gist options
  • Save eladcandroid/0d002548ad3ab5ed94dfcbd23d54c454 to your computer and use it in GitHub Desktop.
Save eladcandroid/0d002548ad3ab5ed94dfcbd23d54c454 to your computer and use it in GitHub Desktop.
Angular Singleton Pattern Example - Configuration Service and Logger Service with single instance guarantee
// Singleton Pattern Example - Configuration and Logger Services
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { APP_INITIALIZER } from '@angular/core';
// Configuration Model
export interface AppConfig {
apiUrl: string;
version: string;
features: {
enableAnalytics: boolean;
enableChat: boolean;
enableDarkMode: boolean;
};
environment: 'development' | 'staging' | 'production';
}
// SINGLETON CONFIGURATION SERVICE
// Ensures only one instance exists across the entire application
@Injectable({
providedIn: 'root' // This ensures singleton at root level
})
export class ConfigurationService {
private static instance: ConfigurationService;
private config: AppConfig;
private configLoaded = false;
constructor(private http: HttpClient) {
// Ensure single instance
if (ConfigurationService.instance) {
return ConfigurationService.instance;
}
ConfigurationService.instance = this;
console.log('ConfigurationService singleton instance created');
}
// Load configuration once at app startup
async loadConfiguration(): Promise<AppConfig> {
if (this.configLoaded) {
return this.config;
}
try {
this.config = await this.http.get<AppConfig>('/assets/config.json').toPromise();
this.configLoaded = true;
console.log('Configuration loaded:', this.config);
return this.config;
} catch (error) {
console.error('Failed to load configuration, using defaults', error);
this.config = this.getDefaultConfig();
this.configLoaded = true;
return this.config;
}
}
getConfig(): AppConfig {
if (!this.configLoaded) {
throw new Error('Configuration not loaded. Call loadConfiguration() first');
}
return this.config;
}
getValue(key: keyof AppConfig): any {
return this.config[key];
}
isFeatureEnabled(feature: keyof AppConfig['features']): boolean {
return this.config.features[feature];
}
private getDefaultConfig(): AppConfig {
return {
apiUrl: 'http://localhost:3000',
version: '1.0.0',
features: {
enableAnalytics: false,
enableChat: false,
enableDarkMode: true
},
environment: 'development'
};
}
}
// Log Level Enum
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
// Log Entry Interface
export interface LogEntry {
timestamp: Date;
level: LogLevel;
message: string;
context?: any;
source?: string;
}
// SINGLETON LOGGER SERVICE
// Maintains a single logging instance for the entire application
@Injectable({
providedIn: 'root'
})
export class LoggerService {
private logs: LogEntry[] = [];
private logLevel: LogLevel = LogLevel.INFO;
private maxLogs = 1000;
private static instance: LoggerService;
constructor(private config: ConfigurationService) {
if (LoggerService.instance) {
return LoggerService.instance;
}
LoggerService.instance = this;
console.log('LoggerService singleton instance created');
// Set log level based on environment
this.initializeLogLevel();
}
private initializeLogLevel(): void {
try {
const appConfig = this.config.getConfig();
switch (appConfig.environment) {
case 'development':
this.logLevel = LogLevel.DEBUG;
break;
case 'staging':
this.logLevel = LogLevel.INFO;
break;
case 'production':
this.logLevel = LogLevel.WARN;
break;
}
} catch {
// Config not loaded yet, use default
this.logLevel = LogLevel.INFO;
}
}
setLogLevel(level: LogLevel): void {
this.logLevel = level;
}
debug(message: string, context?: any, source?: string): void {
this.log(LogLevel.DEBUG, message, context, source);
}
info(message: string, context?: any, source?: string): void {
this.log(LogLevel.INFO, message, context, source);
}
warn(message: string, context?: any, source?: string): void {
this.log(LogLevel.WARN, message, context, source);
}
error(message: string, error?: any, source?: string): void {
this.log(LogLevel.ERROR, message, error, source);
}
private log(level: LogLevel, message: string, context?: any, source?: string): void {
if (level >= this.logLevel) {
const entry: LogEntry = {
timestamp: new Date(),
level,
message,
context,
source: source || this.getCallerSource()
};
this.logs.push(entry);
// Console output with color coding
const prefix = `[${LogLevel[level]}] ${entry.timestamp.toISOString()}`;
const style = this.getConsoleStyle(level);
console.log(`%c${prefix}: ${message}`, style, context || '');
// Maintain max log size
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Send to remote logging in production
if (level >= LogLevel.ERROR && this.isProduction()) {
this.sendToRemoteLogging(entry);
}
}
}
private getConsoleStyle(level: LogLevel): string {
switch (level) {
case LogLevel.DEBUG: return 'color: gray';
case LogLevel.INFO: return 'color: blue';
case LogLevel.WARN: return 'color: orange';
case LogLevel.ERROR: return 'color: red; font-weight: bold';
default: return '';
}
}
private getCallerSource(): string {
// Get caller information from stack trace
const stack = new Error().stack;
if (stack) {
const lines = stack.split('\n');
return lines[3]?.trim() || 'Unknown';
}
return 'Unknown';
}
private isProduction(): boolean {
try {
return this.config.getValue('environment') === 'production';
} catch {
return false;
}
}
private sendToRemoteLogging(entry: LogEntry): void {
// Implementation for remote logging service
// This would send logs to services like Sentry, LogRocket, etc.
console.log('Sending to remote logging:', entry);
}
getLogs(level?: LogLevel): LogEntry[] {
if (level !== undefined) {
return this.logs.filter(log => log.level === level);
}
return [...this.logs];
}
clearLogs(): void {
this.logs = [];
}
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
}
// APP INITIALIZER - Ensures config is loaded before app starts
export function initializeApp(configService: ConfigurationService): () => Promise<any> {
return () => configService.loadConfiguration();
}
// In app.module.ts
/*
@NgModule({
// ...
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigurationService],
multi: true
}
]
})
export class AppModule { }
*/
// USAGE EXAMPLE IN COMPONENT
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<h1>Dashboard</h1>
<p>API URL: {{ apiUrl }}</p>
<p>Version: {{ version }}</p>
<p>Environment: {{ environment }}</p>
<div class="features">
<h3>Features:</h3>
<p>Analytics: {{ features.enableAnalytics ? 'Enabled' : 'Disabled' }}</p>
<p>Chat: {{ features.enableChat ? 'Enabled' : 'Disabled' }}</p>
<p>Dark Mode: {{ features.enableDarkMode ? 'Enabled' : 'Disabled' }}</p>
</div>
<div class="logs">
<h3>Recent Logs ({{ logCount }})</h3>
<button (click)="clearLogs()">Clear Logs</button>
<button (click)="exportLogs()">Export Logs</button>
<button (click)="testLogging()">Test Logging</button>
</div>
</div>
`
})
export class DashboardComponent implements OnInit {
apiUrl: string;
version: string;
environment: string;
features: any;
logCount: number = 0;
constructor(
private config: ConfigurationService,
private logger: LoggerService
) {
// Both services are singletons - same instance everywhere
this.logger.info('DashboardComponent initialized', null, 'DashboardComponent');
}
ngOnInit(): void {
// Get configuration values
const config = this.config.getConfig();
this.apiUrl = config.apiUrl;
this.version = config.version;
this.environment = config.environment;
this.features = config.features;
// Log component lifecycle
this.logger.debug('Dashboard loaded with config', config, 'DashboardComponent');
// Update log count
this.updateLogCount();
}
testLogging(): void {
this.logger.debug('Debug message', { test: true });
this.logger.info('Info message', { timestamp: Date.now() });
this.logger.warn('Warning message', { alert: 'Check this' });
this.logger.error('Error message', new Error('Test error'));
this.updateLogCount();
}
clearLogs(): void {
this.logger.clearLogs();
this.updateLogCount();
}
exportLogs(): void {
const logs = this.logger.exportLogs();
const blob = new Blob([logs], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs-${Date.now()}.json`;
a.click();
}
private updateLogCount(): void {
this.logCount = this.logger.getLogs().length;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment