herolib_python/lib/core/logger/instructions.md
2025-08-05 15:48:18 +02:00

20 KiB

<file_map> /Users/despiegk/code/github/freeflowuniverse/herolib ├── aiprompts │ └── herolib_core │ ├── core_ourtime.md │ ├── core_paths.md │ └── core_text.md └── lib └── core └── logger ├── factory.v ├── log_test.v ├── log.v ├── model.v ├── readme.md └── search.v

</file_map>

<file_contents> File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/factory.v

module logger

import freeflowuniverse.herolib.core.pathlib

pub fn new(path string) !Logger {
	mut p := pathlib.get_dir(path: path, create: true)!
	return Logger{
		path:         p
		lastlog_time: 0
	}
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/log_test.v

module logger

import os
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.core.pathlib

fn testsuite_begin() {
	if os.exists('/tmp/testlogs') {
		os.rmdir_all('/tmp/testlogs')!
	}
}

fn test_logger() {
	mut logger := new('/tmp/testlogs')!

	// Test stdout logging
	logger.log(LogItemArgs{
		cat:       'test-app'
		log:       'This is a test message\nWith a second line\nAnd a third line'
		logtype:   .stdout
		timestamp: ourtime.new('2022-12-05 20:14:35')!
	})!

	// Test error logging
	logger.log(LogItemArgs{
		cat:       'error-test'
		log:       'This is an error\nWith details'
		logtype:   .error
		timestamp: ourtime.new('2022-12-05 20:14:35')!
	})!

	logger.log(LogItemArgs{
		cat:       'test-app'
		log:       'This is a test message\nWith a second line\nAnd a third line'
		logtype:   .stdout
		timestamp: ourtime.new('2022-12-05 20:14:36')!
	})!

	logger.log(LogItemArgs{
		cat:       'error-test'
		log:       '
				This is an error
				
				With details
			'
		logtype:   .error
		timestamp: ourtime.new('2022-12-05 20:14:36')!
	})!

	logger.log(LogItemArgs{
		cat:       'error-test'
		log:       '
				aaa

				bbb
			'
		logtype:   .error
		timestamp: ourtime.new('2022-12-05 22:14:36')!
	})!

	logger.log(LogItemArgs{
		cat:       'error-test'
		log:       '
				aaa2

				bbb2
			'
		logtype:   .error
		timestamp: ourtime.new('2022-12-05 22:14:36')!
	})!

	// Verify log directory exists
	assert os.exists('/tmp/testlogs'), 'Log directory should exist'

	// Get log file
	files := os.ls('/tmp/testlogs')!
	assert files.len == 2

	mut file := pathlib.get_file(
		path:   '/tmp/testlogs/${files[0]}'
		create: false
	)!

	content := file.read()!.trim_space()

	items_stdout := logger.search(
		timestamp_from: ourtime.new('2022-11-1 20:14:35')!
		timestamp_to:   ourtime.new('2025-11-1 20:14:35')!
		logtype:        .stdout
	)!
	assert items_stdout.len == 2

	items_error := logger.search(
		timestamp_from: ourtime.new('2022-11-1 20:14:35')!
		timestamp_to:   ourtime.new('2025-11-1 20:14:35')!
		logtype:        .error
	)!
	assert items_error.len == 4
}

fn testsuite_end() {
	// if os.exists('/tmp/testlogs') {
	// 	os.rmdir_all('/tmp/testlogs')!
	// }
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/log.v

module logger

import os
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.data.ourtime

@[params]
pub struct LogItemArgs {
pub mut:
	timestamp ?ourtime.OurTime
	cat       string
	log       string
	logtype   LogType
}

pub fn (mut l Logger) log(args_ LogItemArgs) ! {
	mut args := args_

	t := args.timestamp or {
		t2 := ourtime.now()
		t2
	}

	// Format category (max 10 chars, ascii only)
	args.cat = texttools.name_fix(args.cat)
	if args.cat.len > 10 {
		return error('category cannot be longer than 10 chars')
	}
	args.cat = texttools.expand(args.cat, 10, ' ')

	args.log = texttools.dedent(args.log).trim_space()

	mut logfile_path := '${l.path.path}/${t.dayhour()}.log'

	// Create log file if it doesn't exist
	if !os.exists(logfile_path) {
		os.write_file(logfile_path, '')!
		l.lastlog_time = 0 // make sure we put time again
	}

	mut f := os.open_append(logfile_path)!

	mut content := ''

	// Add timestamp if we're in a new second
	if t.unix() > l.lastlog_time {
		content += '\n${t.time().format_ss()}\n'
		l.lastlog_time = t.unix()
	}

	// Format log lines
	error_prefix := if args.logtype == .error { 'E' } else { ' ' }
	lines := args.log.split('\n')

	for i, line in lines {
		if i == 0 {
			content += '${error_prefix} ${args.cat} - ${line}\n'
		} else {
			content += '${error_prefix}              ${line}\n'
		}
	}
	f.writeln(content.trim_space_right())!
	f.close()
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/model.v

module logger

import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.core.pathlib

@[heap]
pub struct Logger {
pub mut:
	path         pathlib.Path
	lastlog_time i64 // to see in log format, every second we put a time down, we need to know if we are in a new second (logs can come in much faster)
}

pub struct LogItem {
pub mut:
	timestamp ourtime.OurTime
	cat       string
	log       string
	logtype   LogType
}

pub enum LogType {
	stdout
	error
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/readme.md

# Logger Module

A simple logging system that provides structured logging with search capabilities. 

Logs are stored in hourly files with a consistent format that makes them both human-readable and machine-parseable.

## Features

- Structured logging with categories and error types
- Automatic timestamp management
- Multi-line message support
- Search functionality with filtering options
- Human-readable log format

## Usage

```v
import freeflowuniverse.herolib.core.logger
import freeflowuniverse.herolib.data.ourtime

// Create a new logger
mut l := logger.new(path: '/var/logs')!

// Log a message
l.log(
    cat: 'system',
    log: 'System started successfully',
    logtype: .stdout
)!

// Log an error
l.log(
    cat: 'system',
    log: 'Failed to connect\nRetrying in 5 seconds...',
    logtype: .error
)!

// Search logs
results := l.search(
    timestamp_from: ourtime.now().warp("-24h"), // Last 24 hours
    cat: 'system',                               // Filter by category
    log: 'failed',                              // Search in message content
    logtype: .error,                            // Only error messages
    maxitems: 100                               // Limit results
)!

Log Format

Each log file is named using the format YYYY-MM-DD-HH.log and contains entries in the following format:

21:23:42
 system     - This is a normal log message
 system     - This is a multi-line message
              second line with proper indentation
              third line maintaining alignment
E error_cat - This is an error message
E             second line of error
E             third line of error

Format Rules

  • Time stamps (HH:MM:SS) are written once per second when the log time changes
  • Categories are:
    • Limited to 10 characters maximum
    • Padded with spaces to exactly 10 characters
    • Any - in category names are converted to _
  • Each line starts with either:
    • (space) for normal logs (LogType.stdout)
    • E for error logs (LogType.error)
  • Multi-line messages maintain consistent indentation (14 spaces after the prefix)

File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger/search.v
```v
module logger

import os
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.data.ourtime

@[params]
pub struct SearchArgs {
pub mut:
	timestamp_from ?ourtime.OurTime
	timestamp_to   ?ourtime.OurTime
	cat            string // can be empty
	log            string // any content in here will be looked for
	logtype        LogType
	maxitems       int = 10000
}

pub fn (mut l Logger) search(args_ SearchArgs) ![]LogItem {
	mut args := args_

	// Format category (max 10 chars, ascii only)
	args.cat = texttools.name_fix(args.cat)
	if args.cat.len > 10 {
		return error('category cannot be longer than 10 chars')
	}

	mut timestamp_from := args.timestamp_from or { ourtime.OurTime{} }
	mut timestamp_to := args.timestamp_to or { ourtime.OurTime{} }

	// Get time range
	from_time := timestamp_from.unix()
	to_time := timestamp_to.unix()
	if from_time > to_time {
		return error('from_time cannot be after to_time: ${from_time} <  ${to_time}')
	}

	mut result := []LogItem{}

	// Find log files in time range
	mut files := os.ls(l.path.path)!
	files.sort()

	for file in files {
		if !file.ends_with('.log') {
			continue
		}

		// Parse dayhour from filename
		dayhour := file[..file.len - 4] // remove .log
		file_time := ourtime.new(dayhour)!
		mut current_time := ourtime.OurTime{}
		mut current_item := LogItem{}
		mut collecting := false

		// Skip if file is outside time range
		if file_time.unix() < from_time || file_time.unix() > to_time {
			continue
		}

		// Read and parse log file
		content := os.read_file('${l.path.path}/${file}')!
		lines := content.split('\n')

		for line in lines {
			if result.len >= args.maxitems {
				return result
			}

			line_trim := line.trim_space()
			if line_trim == '' {
				continue
			}

			// Check if this is a timestamp line
			if !(line.starts_with(' ') || line.starts_with('E')) {
				current_time = ourtime.new(line_trim)!
				if collecting {
					process(mut result, current_item, current_time, args, from_time, to_time)!
				}
				collecting = false
				continue
			}

			if collecting && line.len > 14 && line[13] == `-` {
				process(mut result, current_item, current_time, args, from_time, to_time)!
				collecting = false
			}

			// Parse log line
			is_error := line.starts_with('E')
			if !collecting {
				// Start new item
				current_item = LogItem{
					timestamp: current_time
					cat:       line[2..12].trim_space()
					log:       line[15..].trim_space()
					logtype:   if is_error { .error } else { .stdout }
				}
				// println('new current item: ${current_item}')
				collecting = true
			} else {
				// Continuation line
				if line_trim.len < 16 {
					current_item.log += '\n'
				} else {
					current_item.log += '\n' + line[15..]
				}
			}
		}

		// Add last item if collecting
		if collecting {
			process(mut result, current_item, current_time, args, from_time, to_time)!
		}
	}

	return result
}

fn process(mut result []LogItem, current_item LogItem, current_time ourtime.OurTime, args SearchArgs, from_time i64, to_time i64) ! {
	// Add previous item if it matches filters
	log_epoch := current_item.timestamp.unix()
	if log_epoch < from_time || log_epoch > to_time {
		return
	}
	if (args.cat == '' || current_item.cat.trim_space() == args.cat)
		&& (args.log == '' || current_item.log.contains(args.log))
		&& args.logtype == current_item.logtype {
		result << current_item
	}
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/aiprompts/herolib_core/core_ourtime.md

# OurTime Module

The `OurTime` module in V provides flexible time handling, supporting relative and absolute time formats, Unix timestamps, and formatting utilities.

## Key Features
- Create time objects from strings or current time
- Relative time expressions (e.g., `+1h`, `-2d`)
- Absolute time formats (e.g., `YYYY-MM-DD HH:mm:ss`)
- Unix timestamp conversion
- Time formatting and warping

## Basic Usage

```v
import freeflowuniverse.herolib.data.ourtime

// Current time
mut t := ourtime.now()

// From string
t2 := ourtime.new('2022-12-05 20:14:35')!

// Get formatted string
println(t2.str()) // e.g., 2022-12-05 20:14

// Get Unix timestamp
println(t2.unix()) // e.g., 1670271275

Time Formats

Relative Time

Use s (seconds), h (hours), d (days), w (weeks), M (months), Q (quarters), Y (years).

// Create with relative time
mut t := ourtime.new('+1w +2d -4h')!

// Warp existing time
mut t2 := ourtime.now()
t2.warp('+1h')!

Absolute Time

Supports YYYY-MM-DD HH:mm:ss, YYYY-MM-DD HH:mm, YYYY-MM-DD HH, YYYY-MM-DD, DD-MM-YYYY.

t1 := ourtime.new('2022-12-05 20:14:35')!
t2 := ourtime.new('2022-12-05')! // Time defaults to 00:00:00

Methods Overview

Creation

now_time := ourtime.now()
from_string := ourtime.new('2023-01-15')!
from_epoch := ourtime.new_from_epoch(1673788800)

Formatting

mut t := ourtime.now()
println(t.str()) // YYYY-MM-DD HH:mm
println(t.day()) // YYYY-MM-DD
println(t.key()) // YYYY_MM_DD_HH_mm_ss
println(t.md())  // Markdown format

Operations

mut t := ourtime.now()
t.warp('+1h')! // Move 1 hour forward
unix_ts := t.unix()
is_empty := t.empty()

Error Handling

Time parsing methods return a Result type and should be handled with ! or or blocks.

t_valid := ourtime.new('2023-01-01')!
t_invalid := ourtime.new('bad-date') or {
    println('Error: ${err}')
    ourtime.now() // Fallback
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/aiprompts/herolib_core/core_paths.md

# Pathlib Usage Guide

## Overview

The pathlib module provides a comprehensive interface for handling file system operations. Key features include:

- Robust path handling for files, directories, and symlinks
- Support for both absolute and relative paths
- Automatic home directory expansion (~)
- Recursive directory operations
- Path filtering and listing
- File and directory metadata access

## Basic Usage

### Importing pathlib
```v
import freeflowuniverse.herolib.core.pathlib

Creating Path Objects

// Create a Path object for a file
mut file_path := pathlib.get("path/to/file.txt")

// Create a Path object for a directory
mut dir_path := pathlib.get("path/to/directory")

Basic Path Operations

// Get absolute path
abs_path := file_path.absolute()

// Get real path (resolves symlinks)
real_path := file_path.realpath()

// Check if path exists
if file_path.exists() {
    // Path exists
}

Path Properties and Methods

Path Types

// Check if path is a file
if file_path.is_file() {
    // Handle as file
}

// Check if path is a directory
if dir_path.is_dir() {
    // Handle as directory
}

// Check if path is a symlink
if file_path.is_link() {
    // Handle as symlink
}

Path Normalization

// Normalize path (remove extra slashes, resolve . and ..)
normalized_path := file_path.path_normalize()

// Get path directory
dir_path := file_path.path_dir()

// Get path name without extension
name_no_ext := file_path.name_no_ext()

File and Directory Operations

File Operations

// Write to file
file_path.write("Content to write")!

// Read from file
content := file_path.read()!

// Delete file
file_path.delete()!

Directory Operations

// Create directory
mut dir := pathlib.get_dir(
    path: "path/to/new/dir"
    create: true
)!

// List directory contents
mut dir_list := dir.list()!

// Delete directory
dir.delete()!
// Create symlink
file_path.link("path/to/symlink", delete_exists: true)!

// Resolve symlink
real_path := file_path.realpath()

Advanced Operations

Path Copying

// Copy file to destination
file_path.copy(dest: "path/to/destination")!

Recursive Operations

// List directory recursively
mut recursive_list := dir.list(recursive: true)!

// Delete directory recursively
dir.delete()!

Path Filtering

// List files matching pattern
mut filtered_list := dir.list(
    regex: [r".*\.txt$"],
    recursive: true
)!

Best Practices

Error Handling

if file_path.exists() {
    // Safe to operate
} else {
    // Handle missing file
}

File: /Users/despiegk/code/github/freeflowuniverse/herolib/aiprompts/herolib_core/core_text.md
```md
# TextTools Module

The `texttools` module provides a comprehensive set of utilities for text manipulation and processing.

## Functions and Examples:

```v
import freeflowuniverse.herolib.core.texttools

assert hello_world == texttools.name_fix("Hello World!")

Name/Path Processing

  • name_fix(name string) string: Normalizes filenames and paths.
  • name_fix_keepspace(name string) !string: Like name_fix but preserves spaces.
  • name_fix_no_ext(name_ string) string: Removes file extension.
  • name_fix_snake_to_pascal(name string) string: Converts snake_case to PascalCase.
    name := texttools.name_fix_snake_to_pascal("hello_world") // Result: "HelloWorld"
    
  • snake_case(name string) string: Converts PascalCase to snake_case.
    name := texttools.snake_case("HelloWorld") // Result: "hello_world"
    
  • name_split(name string) !(string, string): Splits name into site and page components.

Text Cleaning

  • name_clean(r string) string: Normalizes names by removing special characters.
    name := texttools.name_clean("Hello@World!") // Result: "HelloWorld"
    
  • ascii_clean(r string) string: Removes all non-ASCII characters.
  • remove_empty_lines(text string) string: Removes empty lines from text.
    text := texttools.remove_empty_lines("line1\n\nline2\n\n\nline3") // Result: "line1\nline2\nline3"
    
  • remove_double_lines(text string) string: Removes consecutive empty lines.
  • remove_empty_js_blocks(text string) string: Removes empty code blocks (...).

Command Line Parsing

  • cmd_line_args_parser(text string) ![]string: Parses command line arguments with support for quotes and escaping.
    args := texttools.cmd_line_args_parser("'arg with spaces' --flag=value") // Result: ['arg with spaces', '--flag=value']
    
  • text_remove_quotes(text string) string: Removes quoted sections from text.
  • check_exists_outside_quotes(text string, items []string) bool: Checks if items exist in text outside of quotes.

Text Expansion

  • expand(txt_ string, l int, expand_with string) string: Expands text to a specified length with a given character.

Indentation

  • indent(text string, prefix string) string: Adds indentation prefix to each line.
    text := texttools.indent("line1\nline2", "  ") // Result: "  line1\n  line2\n"
    
  • dedent(text string) string: Removes common leading whitespace from every line.
    text := texttools.dedent("    line1\n    line2") // Result: "line1\nline2"
    

String Validation

  • is_int(text string) bool: Checks if text contains only digits.
  • is_upper_text(text string) bool: Checks if text contains only uppercase letters.

Multiline Processing

  • multiline_to_single(text string) !string: Converts multiline text to a single line with proper escaping.

Text Splitting

  • split_smart(t string, delimiter_ string) []string: Intelligent string splitting that respects quotes.

Tokenization

  • tokenize(text_ string) TokenizerResult: Tokenizes text into meaningful parts.
  • text_token_replace(text string, tofind string, replacewith string) !string: Replaces tokens in text.

Version Parsing

  • version(text_ string) int: Converts version strings to comparable integers.
    ver := texttools.version("v0.4.36") // Result: 4036
    ver = texttools.version("v1.4.36") // Result: 1004036
    

Formatting

  • format_rfc1123(t time.Time) string: Formats a time.Time object into RFC 1123 format.

Array Operations

  • to_array(r string) []string: Converts a comma or newline separated list to an array of strings.
    text := "item1,item2,item3"
    array := texttools.to_array(text) // Result: ['item1', 'item2', 'item3']
    
  • to_array_int(r string) []int: Converts a text list to an array of integers.
  • to_map(mapstring string, line string, delimiter_ string) map[string]string: Intelligent mapping of a line to a map based on a template.
    r := texttools.to_map("name,-,-,-,-,pid,-,-,-,-,path",
        "root   304   0.0  0.0 408185328   1360   ??  S    16Dec23   0:34.06 /usr/sbin/distnoted")
    // Result: {'name': 'root', 'pid': '1360', 'path': '/usr/sbin/distnoted'}
    
</file_contents>
<user_instructions>
create a module in python in location lib/core/logger in herolib_python
which reimplements /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/logger 
all features need to be reimplemented


write me an implementation plan for my coding agent
</user_instructions>