Разработка механизма сопоставления: вывод журнала

Архитектура Go

Добро пожаловать на официальный аккаунт "Keegan Xiaogang" для получения дополнительных статей.


Разработка движка соответствия: начало

Разработка соответствующего движка: версия MVP

Разработка механизма сопоставления: проектирование структуры данных

Разработка Matching Engine: стыковка черного ящика

Разработка механизма сопоставления: процесс расшифровки «черного ящика»

Разработка механизма сопоставления: кодовая реализация процесса

Разработка соответствующего движка: кэш и MQ


требования журнала

Все мы знаем, что журналы играют важную роль в программе, и механизм сопоставления также нуждается в полной функции вывода журнала для облегчения отладки и запроса данных.

Для механизма сопоставления журналы, которые необходимо вывести, в основном включают следующие категории:

  1. Журнал запуска программы, включая журнал успешного подключения к Redis и журнал успешного запуска веб-сервиса;
  2. Журналы данных запроса и ответа интерфейса;
  3. Журнал запуска двигателя;
  4. Журнал двигателя выключен;
  5. Заказ добавлен в журнал orderBook;
  6. журнал записей транзакций;
  7. Журнал результатов отмены.

Кроме того, механизм сопоставления будет генерировать много журналов, поэтому вам также следует выполнить разделение журналов.Разбиение по дате является наиболее распространенным методом разделения журналов, поэтому мы также разделяем журналы с разными датами на разные файлы журналов для сохранения.

Реализовать идеи

Во-первых, все мы знаем, что логи делятся на уровни, например, log4j определяет 8 уровней логов. Однако чаще всего используются 4 уровня, с приоритетом от низкого к высокому:ОТЛАДКА, ИНФОРМАЦИЯ, ПРЕДУПРЕЖДЕНИЕ, ОШИБКА. Как правило, в разных средах устанавливаются разные уровни ведения журналов.Например, уровень DEBUG обычно устанавливается только в средах разработки и тестирования, а для рабочей среды устанавливается значение INFO или выше. При установке на высокий уровень сообщения журнала низкого уровня не печатаются. Чтобы распечатать различные уровни сообщений журнала, вы можете предоставить различные уровни функций печати, такие как предоставлениелог.Отладка(), лог.Информация()и т.п. функция.

Во-вторых, лог нужно выводить в файл для сохранения, поэтому необходимо указать директорию, имя файла и файловый объект для сохраняемого файла. Как правило, каталог сохраненных файлов и работающая программа должны быть объединены, поэтому указанный каталог файлов предпочтительно должен быть относительным путем.

Кроме того, файл должен быть разделен по дате, то есть сообщения журнала разных дат должны сохраняться в разные файлы журнала, поэтому, естественно, должна быть записана дата текущего журнала. И его необходимо регулярно контролировать.Когда обнаруживается, что последняя дата пересекает дату по сравнению с текущей датой журнала, это означает, что журнал необходимо разделить, тогда текущий файл журнала будет скопирован, а новый будет создан файл для сохранения журнала новой даты.

Наконец, если сообщения журнала записываются в файлы, без длительных операций ввода-вывода не обойтись.Если журнал записывается синхронно, это, несомненно, снизит производительность сопоставления.Поэтому лучше записывать журнал асинхронно, что может быть реализовано по буферизованному каналу.

Код

Я перенастроил пакет журнала и создал файл log.go, в котором записан весь код.

Первым шагом является определение нескольких уровней журналов и непосредственное определение их как типов перечисления следующим образом:

type LEVEL byte

const (
	DEBUG LEVEL = iota
	INFO
	WARN
	ERROR
)

Второй шаг — определить структуру журнала, который содержит множество полей, а именно:

type FileLogger struct {
	fileDir        string         // 日志文件保存的目录
	fileName       string         // 日志文件名(无需包含日期和扩展名)
	prefix         string         // 日志消息的前缀
	logLevel       LEVEL          // 日志等级
	logFile        *os.File       // 日志文件
	date           *time.Time     // 日志当前日期
	lg             *log.Logger    // 系统日志对象
	mu             *sync.RWMutex  // 读写锁,在进行日志分割和日志写入时需要锁住
	logChan        chan string    // 日志消息通道,以实现异步写日志
	stopTickerChan chan bool      // 停止定时器的通道
}

Третий шаг, чтобы применить журнал к любому месту в программе, необходимо определить глобальный объект журнала и инициализировать объект журнала. Операция инициализации немного сложна, давайте сначала посмотрим на код:

const DATE_FORMAT = "2006-01-02"

var fileLog *FileLogger

func Init(fileDir, fileName, prefix, level string) error {
	CloseLogger()

	f := &FileLogger{
		fileDir:       fileDir,
		fileName:      fileName,
		prefix:        prefix,
		mu:            new(sync.RWMutex),
		logChan:       make(chan string, 5000),
		stopTikerChan: make(chan bool, 1),
	}

	switch strings.ToUpper(level) {
	case "DEBUG":
		f.logLevel = DEBUG
	case "WARN":
		f.logLevel = WARN
	case "ERROR":
		f.logLevel = ERROR
	default:
		f.logLevel = INFO
	}

	t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
	f.date = &t

	f.isExistOrCreateFileDir()

	fullFileName := filepath.Join(f.fileDir, f.fileName+".log")
	file, err := os.OpenFile(fullFileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil {
		return err
	}
	f.logFile = file

	f.lg = log.New(f.logFile, prefix, log.LstdFlags|log.Lmicroseconds)

	go f.logWriter()
	go f.fileMonitor()

	fileLogger = f

	return nil
}

Логика этой инициализации слишком сложна, поэтому позвольте мне разделить ее. Первый, первый шаг, звонокCloseLogger()Функция, эта функция в основном предназначена для закрытия файлов, закрытия каналов и других операций. Чтобы остановить постоянно зацикливающуюся горутину, закрытие канала — распространенное решение, о котором также упоминалось в предыдущей статье. Затем, поскольку функцию инициализации можно вызывать несколько раз для внесения изменений в конфигурацию, если старая горутина не будет завершена первой, одновременно будет запущено более одной горутины с одной и той же функцией, что, несомненно, вызовет проблемы. Поэтому вам нужно сначала закрыть Logger.Код для закрытия Logger выглядит следующим образом:

func CloseLogger() {
	if fileLogger != nil {
		fileLogger.stopTikerChan <- true
		close(fileLogger.stopTikerChan)
		close(fileLogger.logChan)
		fileLogger.lg = nil
		fileLogger.logFile.Close()
	}
}

После закрытия Регистратора инициализируются и назначаются некоторые поля, среди которых,f.dateОн устанавливается на текущую дату, и последующее решение о том, следует ли разделить, основано на этой дате.f.isExistOrCreateFileDir()Он определит, существует ли каталог журнала, и если нет, то он будет создан. Затем соедините каталог, заданное имя файла и добавленное расширение файла .log, соедините полное имя файла и откройте файл. Затем используйте этот файл для инициализации объекта системного журнала.f.lgТеперь объект фактически вызывается при записи сообщений журнала в файл.Output()функция. Позже запускаются две горутины: одна используется для мониторинга logChan для записи лог-сообщений в файлы, другая используется для регулярного мониторинга, нужно ли разделить файл, а когда нужно разделить, он будет разделен.

Далее давайте взглянем на реализацию этих двух горутин:

func (f *FileLogger) logWriter() {
	defer func() { recover() }()

	for {
		str, ok := <-f.logChan
		if !ok {
			return
		}

		f.mu.RLock()
		f.lg.Output(2, str)
		f.mu.RUnlock()
	}
}

func (f *FileLogger) fileMonitor() {
	defer func() { recover() }()
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			if f.isMustSplit() {
				if err := f.split(); err != nil {
					Error("Log split error: %v\n", err)
				}
			}
		case <-f.stopTikerChan:
			return
		}
	}
}

можно увидетьlogWriter()Цикл считывает сообщения журнала из канала logChan, выходит, когда канал закрывается, в противном случае вызываетf.lg.Output()Выведите лог.fileMonitor(), который создает сообщение, которое отправляется каждые 30 секунд.ticker, когда изticker.CПосле получения данных оценивается необходимость их разделения, и если да, то вызывается функция разделения.f.split(). и изf.stopTikerChanКогда данные получены, это указывает на то, что таймер также остановится.

Затем снова посмотритеisMustSplit()а такжеsplit()функция. isMustSplit() очень прост, всего две строки кода:

func (f *FileLogger) isMustSplit() bool {
	t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
	return t.After(f.date)
}

split() немного сложнее.Сначала добавьте блокировку записи в журнал, чтобы избежать записи журнала во время разделения, затем переименуйте и создайте резервную копию текущего файла журнала, а затем сгенерируйте новый файл для записи новых сообщений журнала.Текущий глобальный журнал объект указывает на новый файл, новую дату и новый объект системного журнала. Код реализации выглядит следующим образом:

func (f *FileLogger) split() error {
	f.mu.Lock()
	defer f.mu.Unlock()

	logFile := filepath.Join(f.fileDir, f.fileName)
	logFileBak := logFile + "-" + f.date.Format(DATE_FORMAT) + ".log"

	if f.logFile != nil {
		f.logFile.Close()
	}

	err := os.Rename(logFile, logFileBak)
	if err != nil {
		return err
	}

	t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
	f.date = &t

	f.logFile, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil {
		return err
	}

	f.lg = log.New(f.logFile, f.prefix, log.LstdFlags|log.Lmicroseconds)

	return nil
}

Наконец, осталось определить некоторые функции, которые получают сообщения журнала, реализация очень проста, чтобыInfo()Например:

func Info(format string, v ...interface{}) {
	_, file, line, _ := runtime.Caller(1)
	if fileLogger.logLevel <= INFO {
		fileLogger.logChan <- fmt.Sprintf("[%v:%v]", filepath.Base(file), line) + fmt.Sprintf("[INFO]"+format, v...)
	}
}

Debug(), Warn(), Error() и другие функции аналогичны, просто следуйте за кошкой и нарисуйте тигра.

На этом наш пакет журналов, который может разделять файлы журналов по дате, завершен.Для остального просто вызовите соответствующую функцию уровня журнала, куда необходимо добавить вывод журнала.

резюме

Суть этой сводки в том, чтобы добавить общий пакет логов, который можно использовать не только в нашем механизме сопоставления, но и в других проектах. Если вы развернете его, вы также сможете разделить его по другим критериям, например по часам или по размеру файла. Желающие могут попробовать сами.

Сегодняшний вопрос для размышления: каковы решения для достижения унифицированного лога вывода данных запроса и ответа интерфейса?

Личный блог автора

Отсканируйте следующий QR-код, чтобы подписаться на официальную учетную запись (имя общедоступной учетной записи: Киган Сяоган)