Files
teleport/pkg/logger/logger.go
Justin Harms d24d1dc5ae Add initial project structure with core functionality
- Created a new Go module named 'teleport' for secure port forwarding.
- Added essential files including .gitignore, LICENSE, and README.md with project details.
- Implemented configuration management with YAML support in config package.
- Developed core client and server functionalities for handling port forwarding.
- Introduced DNS server capabilities and integrated logging with sanitization.
- Established rate limiting and metrics tracking for performance monitoring.
- Included comprehensive tests for core components and functionalities.
- Set up CI workflows for automated testing and release management using Gitea actions.
2025-09-20 18:07:08 -05:00

270 lines
6.9 KiB
Go

package logger
import (
"io"
"os"
"path/filepath"
"regexp"
"sync"
"github.com/sirupsen/logrus"
)
var (
Log *logrus.Logger
once sync.Once
mu sync.RWMutex
)
// Config holds logging configuration
type Config struct {
Level string `yaml:"level"` // debug, info, warn, error
Format string `yaml:"format"` // json, text
File string `yaml:"file"` // log file path (empty for stdout)
MaxSize int `yaml:"max_size"` // max log file size in MB
MaxBackups int `yaml:"max_backups"` // max number of backup files
MaxAge int `yaml:"max_age"` // max age of backup files in days
Compress bool `yaml:"compress"` // compress backup files
}
// Init initializes the global logger with the given configuration
func Init(config Config) error {
mu.Lock()
defer mu.Unlock()
Log = logrus.New()
// Set log level
level, err := logrus.ParseLevel(config.Level)
if err != nil {
level = logrus.InfoLevel
}
Log.SetLevel(level)
// Set log format with sanitization
switch config.Format {
case "json":
Log.SetFormatter(&SanitizedJSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
default:
Log.SetFormatter(&SanitizedTextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
}
// Set output
if config.File != "" {
// Ensure directory exists
dir := filepath.Dir(config.File)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// Open log file with secure permissions (owner read/write only)
file, err := os.OpenFile(config.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return err
}
// Set output to both file and stdout
Log.SetOutput(io.MultiWriter(file, os.Stdout))
} else {
Log.SetOutput(os.Stdout)
}
return nil
}
// GetLogger returns the global logger instance
func GetLogger() *logrus.Logger {
mu.RLock()
if Log != nil {
mu.RUnlock()
return Log
}
mu.RUnlock()
// Initialize with default config if not already initialized
once.Do(func() {
Init(Config{
Level: "info",
Format: "text",
File: "",
})
})
mu.RLock()
defer mu.RUnlock()
return Log
}
// Helper functions for common logging operations
func Debug(args ...interface{}) {
GetLogger().Debug(args...)
}
func Debugf(format string, args ...interface{}) {
GetLogger().Debugf(format, args...)
}
func Info(args ...interface{}) {
GetLogger().Info(args...)
}
func Infof(format string, args ...interface{}) {
GetLogger().Infof(format, args...)
}
func Warn(args ...interface{}) {
GetLogger().Warn(args...)
}
func Warnf(format string, args ...interface{}) {
GetLogger().Warnf(format, args...)
}
func Error(args ...interface{}) {
GetLogger().Error(args...)
}
func Errorf(format string, args ...interface{}) {
GetLogger().Errorf(format, args...)
}
func Fatal(args ...interface{}) {
GetLogger().Fatal(args...)
}
func Fatalf(format string, args ...interface{}) {
GetLogger().Fatalf(format, args...)
}
// WithField creates a new logger entry with a field
func WithField(key string, value interface{}) *logrus.Entry {
return GetLogger().WithField(key, value)
}
// WithFields creates a new logger entry with multiple fields
func WithFields(fields logrus.Fields) *logrus.Entry {
return GetLogger().WithFields(fields)
}
// SanitizedTextFormatter sanitizes log output
type SanitizedTextFormatter struct {
FullTimestamp bool
TimestampFormat string
}
// Format formats the log entry with sanitization
func (f *SanitizedTextFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// Sanitize sensitive data
sanitizedEntry := *entry
sanitizedEntry.Message = sanitizeString(entry.Message)
// Sanitize fields
sanitizedFields := make(logrus.Fields)
for k, v := range entry.Data {
sanitizedFields[k] = sanitizeValue(v)
}
sanitizedEntry.Data = sanitizedFields
// Use default text formatter
formatter := &logrus.TextFormatter{
FullTimestamp: f.FullTimestamp,
TimestampFormat: f.TimestampFormat,
}
return formatter.Format(&sanitizedEntry)
}
// SanitizedJSONFormatter sanitizes JSON log output
type SanitizedJSONFormatter struct {
TimestampFormat string
}
// Format formats the log entry with sanitization
func (f *SanitizedJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// Sanitize sensitive data
sanitizedEntry := *entry
sanitizedEntry.Message = sanitizeString(entry.Message)
// Sanitize fields
sanitizedFields := make(logrus.Fields)
for k, v := range entry.Data {
sanitizedFields[k] = sanitizeValue(v)
}
sanitizedEntry.Data = sanitizedFields
// Use default JSON formatter
formatter := &logrus.JSONFormatter{
TimestampFormat: f.TimestampFormat,
}
return formatter.Format(&sanitizedEntry)
}
// sanitizeString removes or masks sensitive information
func sanitizeString(s string) string {
// Remove potential encryption keys (hex strings longer than 32 chars)
keyPattern := regexp.MustCompile(`[a-fA-F0-9]{32,}`)
s = keyPattern.ReplaceAllString(s, "[REDACTED_KEY]")
// Remove potential passwords and tokens
passwordPattern := regexp.MustCompile(`(?i)(password|pass|pwd|secret|key|token|auth|credential)\s*[:=]\s*[^\s]+`)
s = passwordPattern.ReplaceAllString(s, "$1=[REDACTED]")
// Remove potential API keys and tokens
apiKeyPattern := regexp.MustCompile(`(?i)(api[_-]?key|access[_-]?token|bearer[_-]?token)\s*[:=]\s*[^\s]+`)
s = apiKeyPattern.ReplaceAllString(s, "$1=[REDACTED]")
// Remove potential database connection strings
dbPattern := regexp.MustCompile(`(?i)(mysql|postgres|mongodb|redis)://[^@]+@[^\s]+`)
s = dbPattern.ReplaceAllString(s, "[REDACTED_DB_CONNECTION]")
// Remove potential JWT tokens
jwtPattern := regexp.MustCompile(`eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*`)
s = jwtPattern.ReplaceAllString(s, "[REDACTED_JWT]")
// Remove potential credit card numbers
ccPattern := regexp.MustCompile(`\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b`)
s = ccPattern.ReplaceAllString(s, "[REDACTED_CC]")
// Remove potential SSNs
ssnPattern := regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`)
s = ssnPattern.ReplaceAllString(s, "[REDACTED_SSN]")
// Remove potential IP addresses in sensitive contexts
ipPattern := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`)
s = ipPattern.ReplaceAllString(s, "[REDACTED_IP]")
// Remove potential email addresses
emailPattern := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`)
s = emailPattern.ReplaceAllString(s, "[REDACTED_EMAIL]")
return s
}
// sanitizeValue sanitizes various value types
func sanitizeValue(v interface{}) interface{} {
switch val := v.(type) {
case string:
return sanitizeString(val)
case []byte:
return "[BINARY_DATA]"
case map[string]interface{}:
sanitized := make(map[string]interface{})
for k, v := range val {
sanitized[k] = sanitizeValue(v)
}
return sanitized
case []interface{}:
sanitized := make([]interface{}, len(val))
for i, v := range val {
sanitized[i] = sanitizeValue(v)
}
return sanitized
default:
return v
}
}