diff --git a/source/telemetry/README.md b/source/telemetry/README.md new file mode 100644 index 000000000..0843bced6 --- /dev/null +++ b/source/telemetry/README.md @@ -0,0 +1,461 @@ +# MetaCall Telemetry Library + +A comprehensive logging and metrics telemetry system for the MetaCall polyglot runtime. + +## Features + +### Logging System +- **Multiple Log Levels**: TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL +- **Flexible Handlers**: Console, File, Syslog, Network, and Custom handlers +- **Multiple Output Formats**: Plain text, JSON, XML, and ANSI colored output +- **Log Rotation**: Support for size-based, time-based, daily, weekly, and monthly rotation +- **Filtering**: Category-based and level-based filtering with custom callbacks +- **Thread-Safe**: Built with concurrent access in mind +- **Async Logging**: Optional asynchronous logging for high-performance scenarios + +### Metrics System +- **Metric Types**: + - **Counters**: Monotonically increasing values (e.g., request counts, error counts) + - **Gauges**: Values that can increase or decrease (e.g., memory usage, active connections) + - **Histograms**: Statistical distributions with configurable buckets +- **Labels**: Multi-dimensional metrics with key-value labels +- **Export Formats**: Prometheus, JSON, StatsD, InfluxDB, and Graphite +- **Metric Registry**: Centralized metric management +- **Aggregation**: Built-in support for SUM, AVG, MIN, MAX, COUNT, and RATE +- **Auto-Export**: Configurable exporters with push intervals + +### Language Bindings +- **C**: Native implementation with full API access +- **Python**: Pythonic interface with decorators and context managers +- **JavaScript/Node.js**: Promise-based async API +- **Java**: Thread-safe implementation with builder patterns + +## Architecture + +``` +telemetry/ +├── include/telemetry/ # Public headers +│ ├── telemetry_api.h # API export definitions +│ ├── telemetry_log.h # Logging system API +│ ├── telemetry_metrics.h # Metrics system API +│ └── telemetry_config.h # Configuration management +├── source/ # Implementation +│ ├── telemetry_log.c # Logging implementation +│ └── telemetry_metrics.c # Metrics implementation +├── bindings/ # Language bindings +│ ├── python/ # Python bindings +│ ├── javascript/ # JavaScript/Node.js bindings +│ └── java/ # Java bindings +└── test/ # Test suite + ├── test_log.c # C logging tests + ├── test_metrics.c # C metrics tests + ├── test_telemetry.py # Python tests + └── test_telemetry.js # JavaScript tests +``` + +## Quick Start + +### C API + +```c +#include +#include + +int main() { + // Initialize telemetry + telemetry_log_initialize(); + telemetry_metrics_initialize(); + + // Create and register a console handler + telemetry_log_handler handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_INFO + ); + telemetry_log_handler_register(handler); + + // Log messages + TELEMETRY_LOG_INFO("app", "Application started"); + TELEMETRY_LOG_DEBUG("app", "Processing request"); + TELEMETRY_LOG_ERROR("app", "Error occurred"); + + // Create and use metrics + telemetry_metric counter = telemetry_metric_counter_create( + "requests_total", + "Total requests", + "requests" + ); + telemetry_metric_register(counter); + telemetry_metric_counter_increment(counter, 1); + + // Cleanup + telemetry_log_shutdown(); + telemetry_metrics_shutdown(); + + return 0; +} +``` + +### Python API + +```python +from telemetry import Logger, Counter, MetricRegistry + +# Initialize +Logger.initialize() +MetricRegistry.initialize() + +# Logging +Logger.info("app", "Application started") +Logger.error("app", "An error occurred") + +# Category logger +logger = Logger.get_logger("myapp") +logger.info("Processing request") + +# Metrics +counter = Counter("requests_total", "Total requests", "requests") +MetricRegistry.register(counter) +counter.increment() + +# Decorators +@timed("function_duration") +def process_request(): + # Your code here + pass + +@counted("function_calls") +def api_handler(): + # Your code here + pass +``` + +### JavaScript API + +```javascript +const { Logger, LogLevel, Counter, MetricRegistry } = require('telemetry'); + +// Initialize +Logger.initialize(); +MetricRegistry.initialize(); + +// Logging +Logger.setLevel(LogLevel.DEBUG); +Logger.info("app", "Application started"); +Logger.error("app", "An error occurred"); + +// Category logger +const logger = getLogger("myapp"); +logger.info("Processing request"); + +// Metrics +const counter = new Counter("requests_total", "Total requests", "requests"); +MetricRegistry.register(counter); +counter.increment(); + +// Export metrics +const prometheusData = MetricRegistry.exportAll(MetricExportFormat.PROMETHEUS); +console.log(prometheusData); +``` + +### Java API + +```java +import com.metacall.telemetry.*; + +public class Example { + public static void main(String[] args) { + // Initialize + Logger.initialize(); + MetricRegistry.initialize(); + + // Logging + Logger.setLevel(LogLevel.DEBUG); + Logger.info("app", "Application started"); + Logger.error("app", "An error occurred"); + + // Category logger + CategoryLogger logger = Logger.getLogger("myapp"); + logger.info("Processing request"); + + // Metrics + Counter counter = new Counter("requests_total", "Total requests", "requests"); + MetricRegistry.register(counter); + counter.increment(); + + // Export metrics + String prometheusData = MetricRegistry.exportAll(MetricExportFormat.PROMETHEUS); + System.out.println(prometheusData); + + // Cleanup + Logger.shutdown(); + MetricRegistry.shutdown(); + } +} +``` + +## Configuration + +### JSON Configuration + +```json +{ + "enabled": true, + "log_level": "INFO", + "async_logging": false, + "handlers": [ + { + "type": "CONSOLE", + "min_level": "INFO", + "format": "COLORED", + "enabled": true + }, + { + "type": "FILE", + "min_level": "DEBUG", + "format": "JSON", + "file_path": "/var/log/metacall/app.log", + "rotation": "DAILY", + "max_file_size": 10485760, + "enabled": true + } + ], + "metrics_enabled": true, + "exporters": [ + { + "format": "PROMETHEUS", + "endpoint": "/tmp/metrics.txt", + "push_interval": 60, + "enabled": true + } + ] +} +``` + +### Loading Configuration + +```c +// Load from file +telemetry_config config = telemetry_config_load_from_file("config.json"); +telemetry_config_apply(config); + +// Load from string +const char *json_config = "{...}"; +telemetry_config config = telemetry_config_load_from_string(json_config); +telemetry_config_apply(config); +``` + +## Log Formats + +### Plain Text +``` +[2025-10-01 12:34:56] [INFO] [app] Application started (main.c:42 in main) +``` + +### JSON +```json +{"timestamp":"2025-10-01 12:34:56","level":"INFO","category":"app","message":"Application started","file":"main.c","line":42,"function":"main"} +``` + +### XML +```xml +2025-10-01 12:34:56INFOappApplication startedmain.c42main +``` + +### Colored (ANSI) +Automatically colors output based on log level (errors in red, warnings in yellow, etc.) + +## Metric Export Formats + +### Prometheus +``` +# HELP requests_total Total HTTP requests +# TYPE requests_total counter +requests_total{method="GET",status="200"} 1234 + +# HELP response_time_seconds HTTP response time +# TYPE response_time_seconds histogram +response_time_seconds_bucket{le="0.1"} 100 +response_time_seconds_bucket{le="0.5"} 450 +response_time_seconds_bucket{le="1.0"} 890 +response_time_seconds_sum 423.5 +response_time_seconds_count 1000 +``` + +### JSON +```json +[ + { + "name": "requests_total", + "type": "counter", + "description": "Total HTTP requests", + "unit": "requests", + "value": 1234, + "labels": { + "method": "GET", + "status": "200" + } + } +] +``` + +### InfluxDB Line Protocol +``` +requests_total,method=GET,status=200 value=1234i +response_time,endpoint=/api value=0.142 +``` + +### StatsD +``` +requests_total:1234|c +response_time:142|ms +memory_usage:1024|g +``` + +### Graphite +``` +requests_total 1234 1633089296 +response_time.p50 0.142 1633089296 +memory_usage 1024 1633089296 +``` + +## Performance Considerations + +1. **Async Logging**: Enable for high-throughput scenarios + ```c + telemetry_log_set_async(1); + ``` + +2. **Log Level Filtering**: Set appropriate minimum levels to reduce overhead + ```c + telemetry_log_set_level(TELEMETRY_LOG_LEVEL_WARNING); + ``` + +3. **Metric Sampling**: For high-cardinality metrics, consider sampling + ```c + if (rand() % 100 < sample_rate) { + telemetry_metric_counter_increment(counter, 1); + } + ``` + +4. **Label Cardinality**: Avoid high-cardinality labels (e.g., user IDs, timestamps) + - Good: method, status_code, endpoint + - Bad: user_id, request_id, timestamp + +## Thread Safety + +All telemetry operations are thread-safe by design: +- Log handlers use internal locking +- Metrics use atomic operations where possible +- Registry operations are protected by mutexes + +## Building + +### CMake +```bash +mkdir build +cd build +cmake .. +make +make test +``` + +### Integration with MetaCall +The telemetry library is automatically built as part of the MetaCall core build process. + +## Testing + +### Running C Tests +```bash +./test_log +./test_metrics +``` + +### Running Python Tests +```bash +python3 test_telemetry.py +``` + +### Running JavaScript Tests +```bash +node test_telemetry.js +``` + +### Running Java Tests +```bash +javac com/metacall/telemetry/*.java +java -cp . com.metacall.telemetry.TestTelemetry +``` + +## Best Practices + +1. **Use Structured Logging**: Include context in log messages + ```c + TELEMETRY_LOG_INFO("http", "Request processed: method=%s path=%s status=%d duration=%dms", + method, path, status, duration); + ``` + +2. **Create Category Loggers**: Organize logs by component + ```python + logger = get_logger("database") + logger.info("Connected to database") + ``` + +3. **Use Metric Labels Wisely**: Keep cardinality low + ```java + counter.addLabel("method", "GET"); + counter.addLabel("status", "200"); + // Avoid: counter.addLabel("user_id", userId); + ``` + +4. **Monitor Export Performance**: Large metric sets may need optimization + ```c + // Export periodically rather than on every request + telemetry_metric_exporter_create(TELEMETRY_METRIC_EXPORT_PROMETHEUS, + "/metrics", 60); // 60 second interval + ``` + +5. **Clean Up Resources**: Always shutdown properly + ```c + telemetry_log_flush(); + telemetry_log_shutdown(); + telemetry_metrics_shutdown(); + ``` + +## Troubleshooting + +### Logs Not Appearing +- Check log level settings +- Verify handler is registered +- Ensure handler is enabled + +### Metrics Not Exporting +- Verify exporter is registered +- Check file permissions for file exporters +- Ensure metrics are registered before export + +### Performance Issues +- Enable async logging +- Reduce log level in production +- Sample high-frequency metrics +- Monitor label cardinality + +## Contributing + +Contributions are welcome! Please ensure: +- All tests pass +- Code follows project style +- Documentation is updated +- Thread safety is maintained + +## License + +GNU Lesser General Public License v3.0 or later + +Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + +## Support + +- Documentation: https://github.com/metacall/core/docs +- Issues: https://github.com/metacall/core/issues +- Community: https://t.me/metacall +- Discord: https://discord.gg/upwP4mwJWa diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/CategoryLogger.java b/source/telemetry/bindings/java/com/metacall/telemetry/CategoryLogger.java new file mode 100644 index 000000000..396802f98 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/CategoryLogger.java @@ -0,0 +1,52 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +/** + * Logger scoped to a specific category + */ +public class CategoryLogger { + private final String category; + + public CategoryLogger(String category) { + this.category = category; + } + + public String getCategory() { + return category; + } + + public void trace(String message) { + Logger.trace(category, message); + } + + public void debug(String message) { + Logger.debug(category, message); + } + + public void info(String message) { + Logger.info(category, message); + } + + public void warning(String message) { + Logger.warning(category, message); + } + + public void error(String message) { + Logger.error(category, message); + } + + public void critical(String message) { + Logger.critical(category, message); + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/Counter.java b/source/telemetry/bindings/java/com/metacall/telemetry/Counter.java new file mode 100644 index 000000000..4aae25c18 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/Counter.java @@ -0,0 +1,68 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Counter metric - monotonically increasing value + */ +public class Counter extends Metric { + private final AtomicLong value; + + public Counter(String name) { + this(name, "", ""); + } + + public Counter(String name, String description) { + this(name, description, ""); + } + + public Counter(String name, String description, String unit) { + super(name, description, unit); + this.value = new AtomicLong(0); + } + + /** + * Increment the counter by 1 + */ + public void increment() { + increment(1); + } + + /** + * Increment the counter by a specific value + * @param delta The value to add + */ + public void increment(long delta) { + // TODO: Call native C library function + value.addAndGet(delta); + } + + /** + * Get the current counter value + * @return The current value + */ + public long get() { + // TODO: Call native C library function + return value.get(); + } + + /** + * Reset the counter to zero + */ + public void reset() { + // TODO: Call native C library function + value.set(0); + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/Gauge.java b/source/telemetry/bindings/java/com/metacall/telemetry/Gauge.java new file mode 100644 index 000000000..7db3e082e --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/Gauge.java @@ -0,0 +1,118 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Gauge metric - value that can go up and down + */ +public class Gauge extends Metric { + private final AtomicReference value; + private volatile double minValue; + private volatile double maxValue; + + public Gauge(String name) { + this(name, "", ""); + } + + public Gauge(String name, String description) { + this(name, description, ""); + } + + public Gauge(String name, String description, String unit) { + super(name, description, unit); + this.value = new AtomicReference<>(0.0); + this.minValue = Double.MAX_VALUE; + this.maxValue = -Double.MAX_VALUE; + } + + /** + * Set the gauge value + * @param newValue The new value + */ + public void set(double newValue) { + // TODO: Call native C library function + value.set(newValue); + updateMinMax(newValue); + } + + /** + * Increment the gauge by 1.0 + */ + public void increment() { + increment(1.0); + } + + /** + * Increment the gauge by a specific value + * @param delta The value to add + */ + public void increment(double delta) { + double newValue; + do { + double current = value.get(); + newValue = current + delta; + } while (!value.compareAndSet(value.get(), newValue)); + updateMinMax(newValue); + } + + /** + * Decrement the gauge by 1.0 + */ + public void decrement() { + decrement(1.0); + } + + /** + * Decrement the gauge by a specific value + * @param delta The value to subtract + */ + public void decrement(double delta) { + increment(-delta); + } + + /** + * Get the current gauge value + * @return The current value + */ + public double get() { + // TODO: Call native C library function + return value.get(); + } + + /** + * Get the minimum observed value + * @return The minimum value + */ + public double getMin() { + return minValue; + } + + /** + * Get the maximum observed value + * @return The maximum value + */ + public double getMax() { + return maxValue; + } + + private void updateMinMax(double newValue) { + if (newValue < minValue) { + minValue = newValue; + } + if (newValue > maxValue) { + maxValue = newValue; + } + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/HandlerType.java b/source/telemetry/bindings/java/com/metacall/telemetry/HandlerType.java new file mode 100644 index 000000000..f51fbdc92 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/HandlerType.java @@ -0,0 +1,9 @@ +/* + * Telemetry Library Java Bindings + */ + +package com.metacall.telemetry; + +public enum HandlerType { + CONSOLE, FILE, SYSLOG, NETWORK, CUSTOM +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/Histogram.java b/source/telemetry/bindings/java/com/metacall/telemetry/Histogram.java new file mode 100644 index 000000000..464cf3e64 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/Histogram.java @@ -0,0 +1,131 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Histogram metric - statistical distribution + */ +public class Histogram extends Metric { + private final double[] buckets; + private final AtomicLong[] bucketCounts; + private final AtomicLong totalCount; + private final AtomicReference sum; + private volatile double min; + private volatile double max; + + private static final double[] DEFAULT_BUCKETS = { + 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0 + }; + + public Histogram(String name) { + this(name, "", "", DEFAULT_BUCKETS); + } + + public Histogram(String name, String description) { + this(name, description, "", DEFAULT_BUCKETS); + } + + public Histogram(String name, String description, String unit) { + this(name, description, unit, DEFAULT_BUCKETS); + } + + public Histogram(String name, String description, String unit, double[] buckets) { + super(name, description, unit); + this.buckets = Arrays.copyOf(buckets, buckets.length); + this.bucketCounts = new AtomicLong[buckets.length]; + for (int i = 0; i < buckets.length; i++) { + this.bucketCounts[i] = new AtomicLong(0); + } + this.totalCount = new AtomicLong(0); + this.sum = new AtomicReference<>(0.0); + this.min = Double.MAX_VALUE; + this.max = -Double.MAX_VALUE; + } + + /** + * Observe a value + * @param value The value to observe + */ + public void observe(double value) { + // TODO: Call native C library function + + // Update sum and count + double oldSum; + do { + oldSum = sum.get(); + } while (!sum.compareAndSet(oldSum, oldSum + value)); + totalCount.incrementAndGet(); + + // Update min and max + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + + // Update buckets + for (int i = 0; i < buckets.length; i++) { + if (value <= buckets[i]) { + bucketCounts[i].incrementAndGet(); + } + } + } + + /** + * Get histogram statistics + * @return Statistics object + */ + public HistogramStats getStats() { + // TODO: Call native C library function + return new HistogramStats(totalCount.get(), sum.get(), min, max); + } + + /** + * Get the bucket configuration + * @return Array of bucket upper bounds + */ + public double[] getBuckets() { + return Arrays.copyOf(buckets, buckets.length); + } + + /** + * Get the count for a specific bucket + * @param index The bucket index + * @return The count for that bucket + */ + public long getBucketCount(int index) { + if (index < 0 || index >= bucketCounts.length) { + throw new IndexOutOfBoundsException("Bucket index out of range"); + } + return bucketCounts[index].get(); + } + + /** + * Reset the histogram + */ + public void reset() { + // TODO: Call native C library function + totalCount.set(0); + sum.set(0.0); + min = Double.MAX_VALUE; + max = -Double.MAX_VALUE; + for (AtomicLong count : bucketCounts) { + count.set(0); + } + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/HistogramStats.java b/source/telemetry/bindings/java/com/metacall/telemetry/HistogramStats.java new file mode 100644 index 000000000..6d9e5c616 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/HistogramStats.java @@ -0,0 +1,56 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +/** + * Statistics for histogram metrics + */ +public class HistogramStats { + private final long count; + private final double sum; + private final double min; + private final double max; + + public HistogramStats(long count, double sum, double min, double max) { + this.count = count; + this.sum = sum; + this.min = min; + this.max = max; + } + + public long getCount() { + return count; + } + + public double getSum() { + return sum; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + public double getAverage() { + return count > 0 ? sum / count : 0.0; + } + + @Override + public String toString() { + return String.format("HistogramStats{count=%d, sum=%.6f, min=%.6f, max=%.6f, avg=%.6f}", + count, sum, min, max, getAverage()); + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/LogFormatter.java b/source/telemetry/bindings/java/com/metacall/telemetry/LogFormatter.java new file mode 100644 index 000000000..2087f9be0 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/LogFormatter.java @@ -0,0 +1,9 @@ +/* + * Telemetry Library Java Bindings + */ + +package com.metacall.telemetry; + +public class LogFormatter { + // Placeholder class +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/LogHandler.java b/source/telemetry/bindings/java/com/metacall/telemetry/LogHandler.java new file mode 100644 index 000000000..f63f85fe7 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/LogHandler.java @@ -0,0 +1,79 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +/** + * Log handler for processing log entries + */ +public class LogHandler { + private final HandlerType handlerType; + private final LogLevel minLevel; + private LogFormatter formatter; + private String filePath; + private RotationPolicy rotation; + private long maxFileSize; + + public LogHandler(HandlerType handlerType, LogLevel minLevel) { + this.handlerType = handlerType; + this.minLevel = minLevel; + this.rotation = RotationPolicy.NONE; + this.maxFileSize = 0; + } + + public void configureFile(String filePath, RotationPolicy rotation, long maxSize) { + if (handlerType != HandlerType.FILE) { + throw new IllegalStateException("configureFile only works with FILE handlers"); + } + this.filePath = filePath; + this.rotation = rotation; + this.maxFileSize = maxSize; + // TODO: Call native C library function + } + + public void setFormatter(LogFormatter formatter) { + this.formatter = formatter; + // TODO: Call native C library function + } + + public LogFormatter getFormatter() { + return formatter; + } + + public HandlerType getHandlerType() { + return handlerType; + } + + public LogLevel getMinLevel() { + return minLevel; + } + + public String getFilePath() { + return filePath; + } + + public RotationPolicy getRotation() { + return rotation; + } + + public long getMaxFileSize() { + return maxFileSize; + } + + protected void register() { + // TODO: Call native C library function + } + + protected void unregister() { + // TODO: Call native C library function + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/LogLevel.java b/source/telemetry/bindings/java/com/metacall/telemetry/LogLevel.java new file mode 100644 index 000000000..93c66afc7 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/LogLevel.java @@ -0,0 +1,44 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +/** + * Log severity levels + */ +public enum LogLevel { + TRACE(0), + DEBUG(1), + INFO(2), + WARNING(3), + ERROR(4), + CRITICAL(5); + + private final int value; + + LogLevel(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static LogLevel fromValue(int value) { + for (LogLevel level : values()) { + if (level.value == value) { + return level; + } + } + return INFO; + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/Logger.java b/source/telemetry/bindings/java/com/metacall/telemetry/Logger.java new file mode 100644 index 000000000..aebf8adb1 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/Logger.java @@ -0,0 +1,212 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Main logging interface for the telemetry system + */ +public class Logger { + private static boolean initialized = false; + private static LogLevel globalLevel = LogLevel.INFO; + private static final List handlers = new ArrayList<>(); + private static final ReentrantLock lock = new ReentrantLock(); + + /** + * Initialize the logging system + */ + public static void initialize() { + if (!initialized) { + lock.lock(); + try { + if (!initialized) { + // TODO: Call native C library telemetry_log_initialize() + initialized = true; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Shutdown the logging system + */ + public static void shutdown() { + if (initialized) { + lock.lock(); + try { + if (initialized) { + // TODO: Call native C library telemetry_log_shutdown() + handlers.clear(); + initialized = false; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Set the global log level + * @param level The minimum log level to process + */ + public static void setLevel(LogLevel level) { + initialize(); + lock.lock(); + try { + globalLevel = level; + // TODO: Call native C library function + } finally { + lock.unlock(); + } + } + + /** + * Get the global log level + * @return The current global log level + */ + public static LogLevel getLevel() { + initialize(); + return globalLevel; + } + + /** + * Log a trace message + * @param category Log category + * @param message Log message + */ + public static void trace(String category, String message) { + log(LogLevel.TRACE, category, message); + } + + /** + * Log a debug message + * @param category Log category + * @param message Log message + */ + public static void debug(String category, String message) { + log(LogLevel.DEBUG, category, message); + } + + /** + * Log an info message + * @param category Log category + * @param message Log message + */ + public static void info(String category, String message) { + log(LogLevel.INFO, category, message); + } + + /** + * Log a warning message + * @param category Log category + * @param message Log message + */ + public static void warning(String category, String message) { + log(LogLevel.WARNING, category, message); + } + + /** + * Log an error message + * @param category Log category + * @param message Log message + */ + public static void error(String category, String message) { + log(LogLevel.ERROR, category, message); + } + + /** + * Log a critical message + * @param category Log category + * @param message Log message + */ + public static void critical(String category, String message) { + log(LogLevel.CRITICAL, category, message); + } + + /** + * Internal logging method + * @param level Log level + * @param category Log category + * @param message Log message + */ + private static void log(LogLevel level, String category, String message) { + initialize(); + + if (level.ordinal() < globalLevel.ordinal()) { + return; + } + + // TODO: Call native C library telemetry_log_write() + // Fallback to System.out/err for now + String timestamp = java.time.Instant.now().toString(); + String logMessage = String.format("[%s] [%s] [%s] %s", timestamp, level, category, message); + + if (level.ordinal() >= LogLevel.ERROR.ordinal()) { + System.err.println(logMessage); + } else { + System.out.println(logMessage); + } + } + + /** + * Add a log handler + * @param handler The handler to add + */ + public static void addHandler(LogHandler handler) { + initialize(); + lock.lock(); + try { + handlers.add(handler); + handler.register(); + } finally { + lock.unlock(); + } + } + + /** + * Remove a log handler + * @param handler The handler to remove + */ + public static void removeHandler(LogHandler handler) { + lock.lock(); + try { + if (handlers.remove(handler)) { + handler.unregister(); + } + } finally { + lock.unlock(); + } + } + + /** + * Flush all pending log entries + */ + public static void flush() { + initialize(); + // TODO: Call native C library telemetry_log_flush() + } + + /** + * Get a category-scoped logger + * @param category The category name + * @return A CategoryLogger instance + */ + public static CategoryLogger getLogger(String category) { + return new CategoryLogger(category); + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/Metric.java b/source/telemetry/bindings/java/com/metacall/telemetry/Metric.java new file mode 100644 index 000000000..8a9bdf4db --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/Metric.java @@ -0,0 +1,80 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base class for all metrics + */ +public abstract class Metric { + protected final String name; + protected final String description; + protected final String unit; + protected final Map labels; + + protected Metric(String name, String description, String unit) { + this.name = name; + this.description = description != null ? description : ""; + this.unit = unit != null ? unit : ""; + this.labels = new HashMap<>(); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getUnit() { + return unit; + } + + public void addLabel(String key, String value) { + labels.put(key, value); + // TODO: Call native C library function + } + + public Map getLabels() { + return new HashMap<>(labels); + } + + public String getLabelsString() { + if (labels.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : labels.entrySet()) { + if (!first) { + sb.append(","); + } + sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\""); + first = false; + } + sb.append("}"); + return sb.toString(); + } + + protected void register() { + // TODO: Call native C library function + } + + protected void unregister() { + // TODO: Call native C library function + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/MetricExportFormat.java b/source/telemetry/bindings/java/com/metacall/telemetry/MetricExportFormat.java new file mode 100644 index 000000000..d4e092995 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/MetricExportFormat.java @@ -0,0 +1,9 @@ +/* + * Telemetry Library Java Bindings + */ + +package com.metacall.telemetry; + +public enum MetricExportFormat { + PROMETHEUS, JSON, STATSD, INFLUXDB, GRAPHITE +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/MetricRegistry.java b/source/telemetry/bindings/java/com/metacall/telemetry/MetricRegistry.java new file mode 100644 index 000000000..7fcb33473 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/MetricRegistry.java @@ -0,0 +1,205 @@ +/* + * Telemetry Library Java Bindings + * A Java interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package com.metacall.telemetry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Registry for managing metrics + */ +public class MetricRegistry { + private static boolean initialized = false; + private static final Map metrics = new HashMap<>(); + private static final ReentrantLock lock = new ReentrantLock(); + + /** + * Initialize the metrics system + */ + public static void initialize() { + if (!initialized) { + lock.lock(); + try { + if (!initialized) { + // TODO: Call native C library telemetry_metrics_initialize() + initialized = true; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Shutdown the metrics system + */ + public static void shutdown() { + if (initialized) { + lock.lock(); + try { + if (initialized) { + // TODO: Call native C library telemetry_metrics_shutdown() + metrics.clear(); + initialized = false; + } + } finally { + lock.unlock(); + } + } + } + + /** + * Register a metric + * @param metric The metric to register + */ + public static void register(Metric metric) { + initialize(); + lock.lock(); + try { + if (metrics.containsKey(metric.getName())) { + throw new IllegalArgumentException("Metric '" + metric.getName() + "' already registered"); + } + metrics.put(metric.getName(), metric); + metric.register(); + } finally { + lock.unlock(); + } + } + + /** + * Unregister a metric + * @param metric The metric to unregister + */ + public static void unregister(Metric metric) { + lock.lock(); + try { + if (metrics.remove(metric.getName()) != null) { + metric.unregister(); + } + } finally { + lock.unlock(); + } + } + + /** + * Get a metric by name + * @param name The metric name + * @return The metric, or null if not found + */ + public static Metric get(String name) { + lock.lock(); + try { + return metrics.get(name); + } finally { + lock.unlock(); + } + } + + /** + * Get all registered metrics + * @return Collection of all metrics + */ + public static Collection getAll() { + lock.lock(); + try { + return new HashMap<>(metrics).values(); + } finally { + lock.unlock(); + } + } + + /** + * Export all metrics to string format + * @param format The export format + * @return Exported metrics as string + */ + public static String exportAll(MetricExportFormat format) { + initialize(); + // TODO: Call native C library function + + // Fallback implementation for Prometheus format + if (format == MetricExportFormat.PROMETHEUS) { + StringBuilder sb = new StringBuilder(); + + for (Metric metric : metrics.values()) { + if (!metric.getDescription().isEmpty()) { + sb.append("# HELP ").append(metric.getName()).append(" ") + .append(metric.getDescription()).append("\n"); + } + + if (metric instanceof Counter) { + Counter counter = (Counter) metric; + sb.append("# TYPE ").append(metric.getName()).append(" counter\n"); + sb.append(metric.getName()).append(metric.getLabelsString()) + .append(" ").append(counter.get()).append("\n"); + } else if (metric instanceof Gauge) { + Gauge gauge = (Gauge) metric; + sb.append("# TYPE ").append(metric.getName()).append(" gauge\n"); + sb.append(metric.getName()).append(metric.getLabelsString()) + .append(" ").append(gauge.get()).append("\n"); + } else if (metric instanceof Histogram) { + Histogram histogram = (Histogram) metric; + sb.append("# TYPE ").append(metric.getName()).append(" histogram\n"); + + HistogramStats stats = histogram.getStats(); + sb.append(metric.getName()).append("_sum").append(metric.getLabelsString()) + .append(" ").append(stats.getSum()).append("\n"); + sb.append(metric.getName()).append("_count").append(metric.getLabelsString()) + .append(" ").append(stats.getCount()).append("\n"); + } + + sb.append("\n"); + } + + return sb.toString(); + } + + return ""; + } + + /** + * Reset all metrics + */ + public static void resetAll() { + initialize(); + // TODO: Call native C library function + + lock.lock(); + try { + for (Metric metric : metrics.values()) { + if (metric instanceof Counter) { + ((Counter) metric).reset(); + } else if (metric instanceof Histogram) { + ((Histogram) metric).reset(); + } + } + } finally { + lock.unlock(); + } + } + + /** + * Get the number of registered metrics + * @return Number of metrics + */ + public static int getCount() { + lock.lock(); + try { + return metrics.size(); + } finally { + lock.unlock(); + } + } +} diff --git a/source/telemetry/bindings/java/com/metacall/telemetry/RotationPolicy.java b/source/telemetry/bindings/java/com/metacall/telemetry/RotationPolicy.java new file mode 100644 index 000000000..b90e97bc0 --- /dev/null +++ b/source/telemetry/bindings/java/com/metacall/telemetry/RotationPolicy.java @@ -0,0 +1,9 @@ +/* + * Telemetry Library Java Bindings + */ + +package com.metacall.telemetry; + +public enum RotationPolicy { + NONE, SIZE, TIME, DAILY, WEEKLY, MONTHLY +} diff --git a/source/telemetry/bindings/javascript/telemetry.js b/source/telemetry/bindings/javascript/telemetry.js new file mode 100644 index 000000000..6b76ff1e3 --- /dev/null +++ b/source/telemetry/bindings/javascript/telemetry.js @@ -0,0 +1,683 @@ +/** + * Telemetry Library JavaScript Bindings + * A JavaScript interface for the MetaCall telemetry and logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +'use strict'; + +// Log levels enum +const LogLevel = Object.freeze({ + TRACE: 0, + DEBUG: 1, + INFO: 2, + WARNING: 3, + ERROR: 4, + CRITICAL: 5 +}); + +// Log format types +const LogFormat = Object.freeze({ + TEXT: 0, + JSON: 1, + XML: 2, + COLORED: 3 +}); + +// Handler types +const HandlerType = Object.freeze({ + CONSOLE: 0, + FILE: 1, + SYSLOG: 2, + NETWORK: 3, + CUSTOM: 4 +}); + +// Rotation policy +const RotationPolicy = Object.freeze({ + NONE: 0, + SIZE: 1, + TIME: 2, + DAILY: 3, + WEEKLY: 4, + MONTHLY: 5 +}); + +// Metric types +const MetricType = Object.freeze({ + COUNTER: 0, + GAUGE: 1, + HISTOGRAM: 2, + SUMMARY: 3 +}); + +// Metric export formats +const MetricExportFormat = Object.freeze({ + PROMETHEUS: 0, + JSON: 1, + STATSD: 2, + INFLUXDB: 3, + GRAPHITE: 4 +}); + +// Telemetry error class +class TelemetryError extends Error { + constructor(message) { + super(message); + this.name = 'TelemetryError'; + } +} + +// Log handler class +class LogHandler { + constructor(handlerType, minLevel) { + this.handlerType = handlerType; + this.minLevel = minLevel; + this._handler = null; + this._callback = null; + this._formatter = null; + } + + configureFile(filePath, rotation = RotationPolicy.NONE, maxSize = 0) { + if (this.handlerType !== HandlerType.FILE) { + throw new TelemetryError('configureFile only works with FILE handlers'); + } + // TODO: Call native C library function + this._filePath = filePath; + this._rotation = rotation; + this._maxSize = maxSize; + } + + setCallback(callback) { + if (typeof callback !== 'function') { + throw new TelemetryError('Callback must be a function'); + } + this._callback = callback; + // TODO: Call native C library function + } + + setFormatter(formatter) { + if (!(formatter instanceof LogFormatter)) { + throw new TelemetryError('Formatter must be an instance of LogFormatter'); + } + this._formatter = formatter; + // TODO: Call native C library function + } + + register() { + // TODO: Call native C library function + } + + unregister() { + // TODO: Call native C library function + } +} + +// Log formatter class +class LogFormatter { + constructor(formatType) { + this.formatType = formatType; + this.includeTimestamp = true; + this.includeLevel = true; + this.includeCategory = true; + this.includeLocation = true; + this.includeThreadInfo = false; + this._formatter = null; + } + + configure(options = {}) { + this.includeTimestamp = options.includeTimestamp !== undefined ? options.includeTimestamp : this.includeTimestamp; + this.includeLevel = options.includeLevel !== undefined ? options.includeLevel : this.includeLevel; + this.includeCategory = options.includeCategory !== undefined ? options.includeCategory : this.includeCategory; + this.includeLocation = options.includeLocation !== undefined ? options.includeLocation : this.includeLocation; + this.includeThreadInfo = options.includeThreadInfo !== undefined ? options.includeThreadInfo : this.includeThreadInfo; + // TODO: Call native C library function + } + + setCustomCallback(callback) { + if (typeof callback !== 'function') { + throw new TelemetryError('Callback must be a function'); + } + // TODO: Call native C library function + } +} + +// Logger class +class Logger { + static _initialized = false; + static _handlers = []; + static _level = LogLevel.INFO; + + static initialize() { + if (!Logger._initialized) { + // TODO: Call native C library telemetry_log_initialize() + Logger._initialized = true; + } + } + + static shutdown() { + if (Logger._initialized) { + // TODO: Call native C library telemetry_log_shutdown() + Logger._initialized = false; + } + } + + static setLevel(level) { + Logger.initialize(); + Logger._level = level; + // TODO: Call native C library function + } + + static getLevel() { + Logger.initialize(); + return Logger._level; + } + + static trace(category, message) { + Logger._log(LogLevel.TRACE, category, message); + } + + static debug(category, message) { + Logger._log(LogLevel.DEBUG, category, message); + } + + static info(category, message) { + Logger._log(LogLevel.INFO, category, message); + } + + static warning(category, message) { + Logger._log(LogLevel.WARNING, category, message); + } + + static error(category, message) { + Logger._log(LogLevel.ERROR, category, message); + } + + static critical(category, message) { + Logger._log(LogLevel.CRITICAL, category, message); + } + + static _log(level, category, message) { + Logger.initialize(); + + if (level < Logger._level) { + return; + } + + // TODO: Call native C library telemetry_log_write() + // Fallback to console for now + const timestamp = new Date().toISOString(); + const levelStr = Object.keys(LogLevel).find(key => LogLevel[key] === level); + const logMessage = `[${timestamp}] [${levelStr}] [${category}] ${message}`; + + if (level >= LogLevel.ERROR) { + console.error(logMessage); + } else if (level >= LogLevel.WARNING) { + console.warn(logMessage); + } else if (level >= LogLevel.INFO) { + console.info(logMessage); + } else { + console.log(logMessage); + } + } + + static addHandler(handler) { + if (!(handler instanceof LogHandler)) { + throw new TelemetryError('Handler must be an instance of LogHandler'); + } + Logger._handlers.push(handler); + handler.register(); + } + + static removeHandler(handler) { + const index = Logger._handlers.indexOf(handler); + if (index !== -1) { + handler.unregister(); + Logger._handlers.splice(index, 1); + } + } + + static flush() { + Logger.initialize(); + // TODO: Call native C library telemetry_log_flush() + } +} + +// Base metric class +class Metric { + constructor(name, description = '', unit = '') { + this.name = name; + this.description = description; + this.unit = unit; + this.labels = new Map(); + this._metric = null; + } + + addLabel(key, value) { + this.labels.set(key, value); + // TODO: Call native C library function + } + + register() { + // TODO: Call native C library function + } + + unregister() { + // TODO: Call native C library function + } + + getLabelsString() { + if (this.labels.size === 0) { + return ''; + } + + const labelPairs = Array.from(this.labels.entries()) + .map(([k, v]) => `${k}="${v}"`) + .join(','); + return `{${labelPairs}}`; + } +} + +// Counter metric +class Counter extends Metric { + constructor(name, description = '', unit = '') { + super(name, description, unit); + this._value = 0; + } + + increment(value = 1) { + // TODO: Call native C library function + this._value += value; + } + + get() { + // TODO: Call native C library function + return this._value; + } + + reset() { + // TODO: Call native C library function + this._value = 0; + } +} + +// Gauge metric +class Gauge extends Metric { + constructor(name, description = '', unit = '') { + super(name, description, unit); + this._value = 0.0; + this._min = Infinity; + this._max = -Infinity; + } + + set(value) { + // TODO: Call native C library function + this._value = value; + this._min = Math.min(this._min, value); + this._max = Math.max(this._max, value); + } + + increment(value = 1.0) { + this.set(this._value + value); + } + + decrement(value = 1.0) { + this.set(this._value - value); + } + + get() { + // TODO: Call native C library function + return this._value; + } + + getMin() { + return this._min; + } + + getMax() { + return this._max; + } +} + +// Histogram metric +class Histogram extends Metric { + constructor(name, description = '', unit = '', buckets = null) { + super(name, description, unit); + this.buckets = buckets || [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]; + this._observations = []; + this._bucketCounts = new Array(this.buckets.length).fill(0); + } + + observe(value) { + // TODO: Call native C library function + this._observations.push(value); + + // Update bucket counts + for (let i = 0; i < this.buckets.length; i++) { + if (value <= this.buckets[i]) { + this._bucketCounts[i]++; + } + } + } + + getStats() { + // TODO: Call native C library function + if (this._observations.length === 0) { + return { count: 0, sum: 0, min: 0, max: 0 }; + } + + return { + count: this._observations.length, + sum: this._observations.reduce((a, b) => a + b, 0), + min: Math.min(...this._observations), + max: Math.max(...this._observations) + }; + } + + getBuckets() { + return this.buckets.map((bound, idx) => ({ + upperBound: bound, + count: this._bucketCounts[idx] + })); + } + + reset() { + // TODO: Call native C library function + this._observations = []; + this._bucketCounts.fill(0); + } +} + +// Metric registry +class MetricRegistry { + static _initialized = false; + static _metrics = new Map(); + + static initialize() { + if (!MetricRegistry._initialized) { + // TODO: Call native C library telemetry_metrics_initialize() + MetricRegistry._initialized = true; + } + } + + static shutdown() { + if (MetricRegistry._initialized) { + // TODO: Call native C library telemetry_metrics_shutdown() + MetricRegistry._initialized = false; + } + } + + static register(metric) { + MetricRegistry.initialize(); + + if (!(metric instanceof Metric)) { + throw new TelemetryError('Must register a Metric instance'); + } + + if (MetricRegistry._metrics.has(metric.name)) { + throw new TelemetryError(`Metric '${metric.name}' already registered`); + } + + MetricRegistry._metrics.set(metric.name, metric); + metric.register(); + } + + static unregister(metric) { + if (MetricRegistry._metrics.has(metric.name)) { + metric.unregister(); + MetricRegistry._metrics.delete(metric.name); + } + } + + static get(name) { + return MetricRegistry._metrics.get(name); + } + + static getAll() { + return Array.from(MetricRegistry._metrics.values()); + } + + static exportAll(format = MetricExportFormat.PROMETHEUS) { + MetricRegistry.initialize(); + // TODO: Call native C library function + + // Fallback implementation for Prometheus format + if (format === MetricExportFormat.PROMETHEUS) { + const lines = []; + + for (const metric of MetricRegistry._metrics.values()) { + if (metric.description) { + lines.push(`# HELP ${metric.name} ${metric.description}`); + } + + if (metric instanceof Counter) { + lines.push(`# TYPE ${metric.name} counter`); + lines.push(`${metric.name}${metric.getLabelsString()} ${metric.get()}`); + } else if (metric instanceof Gauge) { + lines.push(`# TYPE ${metric.name} gauge`); + lines.push(`${metric.name}${metric.getLabelsString()} ${metric.get()}`); + } else if (metric instanceof Histogram) { + lines.push(`# TYPE ${metric.name} histogram`); + const stats = metric.getStats(); + const buckets = metric.getBuckets(); + + for (const bucket of buckets) { + const labels = metric.getLabelsString(); + const bucketLabel = labels ? `{le="${bucket.upperBound}",${labels.slice(1)}` : `{le="${bucket.upperBound}"}`; + lines.push(`${metric.name}_bucket${bucketLabel} ${bucket.count}`); + } + + lines.push(`${metric.name}_sum${metric.getLabelsString()} ${stats.sum}`); + lines.push(`${metric.name}_count${metric.getLabelsString()} ${stats.count}`); + } + + lines.push(''); + } + + return lines.join('\n'); + } else if (format === MetricExportFormat.JSON) { + const metrics = []; + + for (const metric of MetricRegistry._metrics.values()) { + const data = { + name: metric.name, + description: metric.description, + unit: metric.unit, + labels: Object.fromEntries(metric.labels) + }; + + if (metric instanceof Counter) { + data.type = 'counter'; + data.value = metric.get(); + } else if (metric instanceof Gauge) { + data.type = 'gauge'; + data.value = metric.get(); + data.min = metric.getMin(); + data.max = metric.getMax(); + } else if (metric instanceof Histogram) { + data.type = 'histogram'; + data.stats = metric.getStats(); + data.buckets = metric.getBuckets(); + } + + metrics.push(data); + } + + return JSON.stringify(metrics, null, 2); + } + + return ''; + } + + static resetAll() { + MetricRegistry.initialize(); + // TODO: Call native C library function + + for (const metric of MetricRegistry._metrics.values()) { + if (typeof metric.reset === 'function') { + metric.reset(); + } + } + } +} + +// Metric exporter +class MetricExporter { + constructor(format, endpoint, pushInterval = 0) { + this.format = format; + this.endpoint = endpoint; + this.pushInterval = pushInterval; + this._exporter = null; + this._intervalHandle = null; + } + + register() { + // TODO: Call native C library function + + if (this.pushInterval > 0) { + this._intervalHandle = setInterval(() => { + this.export(); + }, this.pushInterval * 1000); + } + } + + unregister() { + // TODO: Call native C library function + + if (this._intervalHandle) { + clearInterval(this._intervalHandle); + this._intervalHandle = null; + } + } + + export() { + // TODO: Call native C library function + const data = MetricRegistry.exportAll(this.format); + + // In a real implementation, this would send to endpoint + // For now, just log it + console.log(`Exporting to ${this.endpoint}:`); + console.log(data); + + return data; + } +} + +// Category logger helper +class CategoryLogger { + constructor(category) { + this.category = category; + } + + trace(message) { + Logger.trace(this.category, message); + } + + debug(message) { + Logger.debug(this.category, message); + } + + info(message) { + Logger.info(this.category, message); + } + + warning(message) { + Logger.warning(this.category, message); + } + + error(message) { + Logger.error(this.category, message); + } + + critical(message) { + Logger.critical(this.category, message); + } +} + +// Utility function to get a category logger +function getLogger(category) { + return new CategoryLogger(category); +} + +// Decorators (using function wrappers) +function timed(metricName) { + return function(target, propertyKey, descriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function(...args) { + let histogram = MetricRegistry.get(metricName); + if (!histogram) { + histogram = new Histogram(metricName, `Execution time of ${propertyKey}`, 'seconds'); + MetricRegistry.register(histogram); + } + + const start = Date.now(); + try { + return originalMethod.apply(this, args); + } finally { + const duration = (Date.now() - start) / 1000; + histogram.observe(duration); + } + }; + + return descriptor; + }; +} + +function counted(metricName) { + return function(target, propertyKey, descriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function(...args) { + let counter = MetricRegistry.get(metricName); + if (!counter) { + counter = new Counter(metricName, `Call count of ${propertyKey}`, 'calls'); + MetricRegistry.register(counter); + } + + counter.increment(); + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} + +// Initialize on load +Logger.initialize(); +MetricRegistry.initialize(); + +// Export everything +module.exports = { + // Enums + LogLevel, + LogFormat, + HandlerType, + RotationPolicy, + MetricType, + MetricExportFormat, + + // Classes + TelemetryError, + Logger, + LogHandler, + LogFormatter, + Metric, + Counter, + Gauge, + Histogram, + MetricRegistry, + MetricExporter, + CategoryLogger, + + // Utility functions + getLogger, + timed, + counted +}; diff --git a/source/telemetry/bindings/python/telemetry.py b/source/telemetry/bindings/python/telemetry.py new file mode 100644 index 000000000..3bf88ae4d --- /dev/null +++ b/source/telemetry/bindings/python/telemetry.py @@ -0,0 +1,537 @@ +""" +Telemetry Library Python Bindings +A Python interface for the MetaCall telemetry and logging system. + +Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +""" + +import ctypes +import enum +import os +import sys +from typing import Any, Callable, Dict, List, Optional, Union + + +class LogLevel(enum.IntEnum): + """Log severity levels""" + TRACE = 0 + DEBUG = 1 + INFO = 2 + WARNING = 3 + ERROR = 4 + CRITICAL = 5 + + +class LogFormat(enum.IntEnum): + """Log output format types""" + TEXT = 0 + JSON = 1 + XML = 2 + COLORED = 3 + + +class HandlerType(enum.IntEnum): + """Log handler types""" + CONSOLE = 0 + FILE = 1 + SYSLOG = 2 + NETWORK = 3 + CUSTOM = 4 + + +class RotationPolicy(enum.IntEnum): + """Log rotation policy""" + NONE = 0 + SIZE = 1 + TIME = 2 + DAILY = 3 + WEEKLY = 4 + MONTHLY = 5 + + +class MetricType(enum.IntEnum): + """Metric types""" + COUNTER = 0 + GAUGE = 1 + HISTOGRAM = 2 + SUMMARY = 3 + + +class MetricExportFormat(enum.IntEnum): + """Metric export format types""" + PROMETHEUS = 0 + JSON = 1 + STATSD = 2 + INFLUXDB = 3 + GRAPHITE = 4 + + +class TelemetryError(Exception): + """Base exception for telemetry errors""" + pass + + +class LogHandler: + """Python wrapper for telemetry log handler""" + + def __init__(self, handler_type: HandlerType, min_level: LogLevel): + self.handler_type = handler_type + self.min_level = min_level + self._handler = None + self._callback = None + + def configure_file(self, file_path: str, rotation: RotationPolicy = RotationPolicy.NONE, max_size: int = 0): + """Configure file handler settings""" + if self.handler_type != HandlerType.FILE: + raise TelemetryError("configure_file only works with FILE handlers") + # TODO: Call C library function + pass + + def set_callback(self, callback: Callable): + """Set custom handler callback""" + self._callback = callback + # TODO: Call C library function + pass + + def register(self): + """Register this handler with the logging system""" + # TODO: Call C library function + pass + + def unregister(self): + """Unregister this handler from the logging system""" + # TODO: Call C library function + pass + + +class LogFormatter: + """Python wrapper for telemetry log formatter""" + + def __init__(self, format_type: LogFormat): + self.format_type = format_type + self.include_timestamp = True + self.include_level = True + self.include_category = True + self.include_location = True + self.include_thread_info = False + self._formatter = None + + def configure(self, include_timestamp: bool = True, include_level: bool = True, + include_category: bool = True, include_location: bool = True, + include_thread_info: bool = False): + """Configure formatter options""" + self.include_timestamp = include_timestamp + self.include_level = include_level + self.include_category = include_category + self.include_location = include_location + self.include_thread_info = include_thread_info + # TODO: Call C library function + pass + + +class Logger: + """Main logging interface""" + + _initialized = False + _handlers: List[LogHandler] = [] + + @classmethod + def initialize(cls): + """Initialize the logging system""" + if not cls._initialized: + # TODO: Call C library telemetry_log_initialize() + cls._initialized = True + + @classmethod + def shutdown(cls): + """Shutdown the logging system""" + if cls._initialized: + # TODO: Call C library telemetry_log_shutdown() + cls._initialized = False + + @classmethod + def set_level(cls, level: LogLevel): + """Set the global log level""" + cls.initialize() + # TODO: Call C library function + pass + + @classmethod + def get_level(cls) -> LogLevel: + """Get the global log level""" + cls.initialize() + # TODO: Call C library function + return LogLevel.INFO + + @classmethod + def trace(cls, category: str, message: str): + """Log a trace message""" + cls._log(LogLevel.TRACE, category, message) + + @classmethod + def debug(cls, category: str, message: str): + """Log a debug message""" + cls._log(LogLevel.DEBUG, category, message) + + @classmethod + def info(cls, category: str, message: str): + """Log an info message""" + cls._log(LogLevel.INFO, category, message) + + @classmethod + def warning(cls, category: str, message: str): + """Log a warning message""" + cls._log(LogLevel.WARNING, category, message) + + @classmethod + def error(cls, category: str, message: str): + """Log an error message""" + cls._log(LogLevel.ERROR, category, message) + + @classmethod + def critical(cls, category: str, message: str): + """Log a critical message""" + cls._log(LogLevel.CRITICAL, category, message) + + @classmethod + def _log(cls, level: LogLevel, category: str, message: str): + """Internal logging method""" + cls.initialize() + # TODO: Call C library telemetry_log_write() + # For now, use Python logging as fallback + import logging + py_logger = logging.getLogger(category) + + level_map = { + LogLevel.TRACE: logging.DEBUG, + LogLevel.DEBUG: logging.DEBUG, + LogLevel.INFO: logging.INFO, + LogLevel.WARNING: logging.WARNING, + LogLevel.ERROR: logging.ERROR, + LogLevel.CRITICAL: logging.CRITICAL, + } + + py_logger.log(level_map.get(level, logging.INFO), message) + + @classmethod + def add_handler(cls, handler: LogHandler): + """Add a log handler""" + cls._handlers.append(handler) + handler.register() + + @classmethod + def remove_handler(cls, handler: LogHandler): + """Remove a log handler""" + if handler in cls._handlers: + handler.unregister() + cls._handlers.remove(handler) + + @classmethod + def flush(cls): + """Flush all pending log entries""" + cls.initialize() + # TODO: Call C library telemetry_log_flush() + pass + + +class Metric: + """Base class for all metrics""" + + def __init__(self, name: str, description: str = "", unit: str = ""): + self.name = name + self.description = description + self.unit = unit + self.labels: Dict[str, str] = {} + self._metric = None + + def add_label(self, key: str, value: str): + """Add a label to this metric""" + self.labels[key] = value + # TODO: Call C library function + + def register(self): + """Register this metric with the registry""" + # TODO: Call C library function + pass + + def unregister(self): + """Unregister this metric from the registry""" + # TODO: Call C library function + pass + + +class Counter(Metric): + """Counter metric - monotonically increasing value""" + + def __init__(self, name: str, description: str = "", unit: str = ""): + super().__init__(name, description, unit) + self._value = 0 + + def increment(self, value: int = 1): + """Increment the counter""" + # TODO: Call C library function + self._value += value + + def get(self) -> int: + """Get the current counter value""" + # TODO: Call C library function + return self._value + + def reset(self): + """Reset the counter to zero""" + # TODO: Call C library function + self._value = 0 + + +class Gauge(Metric): + """Gauge metric - value that can go up and down""" + + def __init__(self, name: str, description: str = "", unit: str = ""): + super().__init__(name, description, unit) + self._value = 0.0 + + def set(self, value: float): + """Set the gauge value""" + # TODO: Call C library function + self._value = value + + def increment(self, value: float = 1.0): + """Increment the gauge""" + # TODO: Call C library function + self._value += value + + def decrement(self, value: float = 1.0): + """Decrement the gauge""" + # TODO: Call C library function + self._value -= value + + def get(self) -> float: + """Get the current gauge value""" + # TODO: Call C library function + return self._value + + +class Histogram(Metric): + """Histogram metric - statistical distribution""" + + def __init__(self, name: str, description: str = "", unit: str = "", + buckets: Optional[List[float]] = None): + super().__init__(name, description, unit) + self.buckets = buckets or [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] + self._observations = [] + + def observe(self, value: float): + """Observe a value""" + # TODO: Call C library function + self._observations.append(value) + + def get_stats(self) -> Dict[str, Union[int, float]]: + """Get histogram statistics""" + # TODO: Call C library function + if not self._observations: + return {"count": 0, "sum": 0.0, "min": 0.0, "max": 0.0} + + return { + "count": len(self._observations), + "sum": sum(self._observations), + "min": min(self._observations), + "max": max(self._observations), + } + + def reset(self): + """Reset the histogram""" + # TODO: Call C library function + self._observations.clear() + + +class MetricRegistry: + """Metric registry for managing metrics""" + + _initialized = False + _metrics: Dict[str, Metric] = {} + + @classmethod + def initialize(cls): + """Initialize the metrics system""" + if not cls._initialized: + # TODO: Call C library telemetry_metrics_initialize() + cls._initialized = True + + @classmethod + def shutdown(cls): + """Shutdown the metrics system""" + if cls._initialized: + # TODO: Call C library telemetry_metrics_shutdown() + cls._initialized = False + + @classmethod + def register(cls, metric: Metric): + """Register a metric""" + cls.initialize() + cls._metrics[metric.name] = metric + metric.register() + + @classmethod + def unregister(cls, metric: Metric): + """Unregister a metric""" + if metric.name in cls._metrics: + metric.unregister() + del cls._metrics[metric.name] + + @classmethod + def get(cls, name: str) -> Optional[Metric]: + """Get a metric by name""" + return cls._metrics.get(name) + + @classmethod + def get_all(cls) -> List[Metric]: + """Get all registered metrics""" + return list(cls._metrics.values()) + + @classmethod + def export_all(cls, format_type: MetricExportFormat = MetricExportFormat.PROMETHEUS) -> str: + """Export all metrics to string""" + cls.initialize() + # TODO: Call C library function + + # Fallback implementation + output = [] + for metric in cls._metrics.values(): + if isinstance(metric, Counter): + output.append(f"# TYPE {metric.name} counter") + output.append(f"{metric.name} {metric.get()}") + elif isinstance(metric, Gauge): + output.append(f"# TYPE {metric.name} gauge") + output.append(f"{metric.name} {metric.get()}") + elif isinstance(metric, Histogram): + stats = metric.get_stats() + output.append(f"# TYPE {metric.name} histogram") + output.append(f"{metric.name}_count {stats['count']}") + output.append(f"{metric.name}_sum {stats['sum']}") + + return "\n".join(output) + + @classmethod + def reset_all(cls): + """Reset all metrics""" + cls.initialize() + # TODO: Call C library function + for metric in cls._metrics.values(): + if hasattr(metric, 'reset'): + metric.reset() + + +class MetricExporter: + """Metric exporter for pushing metrics to external systems""" + + def __init__(self, format_type: MetricExportFormat, endpoint: str, push_interval: int = 0): + self.format_type = format_type + self.endpoint = endpoint + self.push_interval = push_interval + self._exporter = None + + def register(self): + """Register this exporter""" + # TODO: Call C library function + pass + + def unregister(self): + """Unregister this exporter""" + # TODO: Call C library function + pass + + def export(self) -> str: + """Export metrics using this exporter""" + # TODO: Call C library function + return MetricRegistry.export_all(self.format_type) + + +# Convenience decorators +def timed(metric_name: str): + """Decorator to time function execution and record in histogram""" + import time + + def decorator(func): + def wrapper(*args, **kwargs): + histogram = MetricRegistry.get(metric_name) + if histogram is None: + histogram = Histogram(metric_name, f"Execution time of {func.__name__}", "seconds") + MetricRegistry.register(histogram) + + start = time.time() + try: + return func(*args, **kwargs) + finally: + duration = time.time() - start + histogram.observe(duration) + + return wrapper + return decorator + + +def counted(metric_name: str): + """Decorator to count function calls""" + def decorator(func): + def wrapper(*args, **kwargs): + counter = MetricRegistry.get(metric_name) + if counter is None: + counter = Counter(metric_name, f"Call count of {func.__name__}", "calls") + MetricRegistry.register(counter) + + counter.increment() + return func(*args, **kwargs) + + return wrapper + return decorator + + +# Module-level convenience functions +def get_logger(category: str) -> 'CategoryLogger': + """Get a logger for a specific category""" + return CategoryLogger(category) + + +class CategoryLogger: + """Logger scoped to a specific category""" + + def __init__(self, category: str): + self.category = category + + def trace(self, message: str): + Logger.trace(self.category, message) + + def debug(self, message: str): + Logger.debug(self.category, message) + + def info(self, message: str): + Logger.info(self.category, message) + + def warning(self, message: str): + Logger.warning(self.category, message) + + def error(self, message: str): + Logger.error(self.category, message) + + def critical(self, message: str): + Logger.critical(self.category, message) + + +# Initialize on import +Logger.initialize() +MetricRegistry.initialize() + + +__all__ = [ + 'LogLevel', 'LogFormat', 'HandlerType', 'RotationPolicy', + 'MetricType', 'MetricExportFormat', 'TelemetryError', + 'Logger', 'LogHandler', 'LogFormatter', + 'Metric', 'Counter', 'Gauge', 'Histogram', + 'MetricRegistry', 'MetricExporter', + 'get_logger', 'CategoryLogger', + 'timed', 'counted' +] diff --git a/source/telemetry/include/telemetry/telemetry_api.h b/source/telemetry/include/telemetry/telemetry_api.h new file mode 100644 index 000000000..00ad06aa8 --- /dev/null +++ b/source/telemetry/include/telemetry/telemetry_api.h @@ -0,0 +1,51 @@ +/* + * Telemetry Library by MetaCall Inc. + * A library for logging and telemetry collection in MetaCall runtime. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#ifndef TELEMETRY_API_H +#define TELEMETRY_API_H 1 + +/* -- Headers -- */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* -- Definitions -- */ + +#if defined(_WIN32) || defined(_WIN64) + #ifdef TELEMETRY_EXPORTS + #define TELEMETRY_API __declspec(dllexport) + #else + #define TELEMETRY_API __declspec(dllimport) + #endif +#elif defined(__GNUC__) && __GNUC__ >= 4 + #define TELEMETRY_API __attribute__((visibility("default"))) +#else + #define TELEMETRY_API +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* TELEMETRY_API_H */ diff --git a/source/telemetry/include/telemetry/telemetry_config.h b/source/telemetry/include/telemetry/telemetry_config.h new file mode 100644 index 000000000..2f4334e6b --- /dev/null +++ b/source/telemetry/include/telemetry/telemetry_config.h @@ -0,0 +1,212 @@ +/* + * Telemetry Library by MetaCall Inc. + * Configuration management for telemetry system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#ifndef TELEMETRY_CONFIG_H +#define TELEMETRY_CONFIG_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +/* -- Headers -- */ + +#include +#include +#include + +/* -- Definitions -- */ + +#define TELEMETRY_CONFIG_MAX_STRING_SIZE 512 +#define TELEMETRY_CONFIG_MAX_HANDLERS 16 +#define TELEMETRY_CONFIG_MAX_EXPORTERS 8 + +/* -- Type Definitions -- */ + +/** + * @brief Configuration for a log handler + */ +struct telemetry_config_handler_type +{ + telemetry_log_handler_type_id type; + telemetry_log_level_id min_level; + telemetry_log_format_id format; + char file_path[TELEMETRY_CONFIG_MAX_STRING_SIZE]; + telemetry_log_rotation_policy_id rotation; + size_t max_file_size; + int enabled; +}; + +/** + * @brief Configuration for a metric exporter + */ +struct telemetry_config_exporter_type +{ + telemetry_metric_export_format_id format; + char endpoint[TELEMETRY_CONFIG_MAX_STRING_SIZE]; + int push_interval; + int enabled; +}; + +/** + * @brief Global telemetry configuration + */ +struct telemetry_config_type +{ + /* Logging configuration */ + telemetry_log_level_id log_level; + int async_logging; + struct telemetry_config_handler_type handlers[TELEMETRY_CONFIG_MAX_HANDLERS]; + size_t handler_count; + + /* Metrics configuration */ + struct telemetry_config_exporter_type exporters[TELEMETRY_CONFIG_MAX_EXPORTERS]; + size_t exporter_count; + int metrics_enabled; + + /* General configuration */ + int enabled; + char config_file_path[TELEMETRY_CONFIG_MAX_STRING_SIZE]; +}; + +typedef struct telemetry_config_type *telemetry_config; + +/* -- Methods -- */ + +/** + * @brief Create a new telemetry configuration with defaults + * @return Pointer to the created configuration, or NULL on failure + */ +TELEMETRY_API telemetry_config telemetry_config_create(void); + +/** + * @brief Destroy a telemetry configuration + * @param config The configuration to destroy + */ +TELEMETRY_API void telemetry_config_destroy(telemetry_config config); + +/** + * @brief Load configuration from a JSON file + * @param file_path Path to the configuration file + * @return Pointer to the loaded configuration, or NULL on failure + */ +TELEMETRY_API telemetry_config telemetry_config_load_from_file(const char *file_path); + +/** + * @brief Load configuration from a JSON string + * @param json_string JSON configuration string + * @return Pointer to the loaded configuration, or NULL on failure + */ +TELEMETRY_API telemetry_config telemetry_config_load_from_string(const char *json_string); + +/** + * @brief Save configuration to a JSON file + * @param config The configuration to save + * @param file_path Path to save the configuration + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_save_to_file(telemetry_config config, const char *file_path); + +/** + * @brief Convert configuration to JSON string + * @param config The configuration to convert + * @param buffer Output buffer + * @param size Buffer size + * @return Number of bytes written, or negative on error + */ +TELEMETRY_API int telemetry_config_to_string(telemetry_config config, char *buffer, size_t size); + +/** + * @brief Apply configuration to the telemetry system + * @param config The configuration to apply + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_apply(telemetry_config config); + +/** + * @brief Add a handler configuration + * @param config The configuration + * @param type Handler type + * @param min_level Minimum log level + * @param format Log format + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_add_handler(telemetry_config config, telemetry_log_handler_type_id type, telemetry_log_level_id min_level, telemetry_log_format_id format); + +/** + * @brief Add an exporter configuration + * @param config The configuration + * @param format Export format + * @param endpoint Export endpoint + * @param push_interval Push interval in seconds + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_add_exporter(telemetry_config config, telemetry_metric_export_format_id format, const char *endpoint, int push_interval); + +/** + * @brief Set log level in configuration + * @param config The configuration + * @param level The log level + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_set_log_level(telemetry_config config, telemetry_log_level_id level); + +/** + * @brief Enable or disable async logging in configuration + * @param config The configuration + * @param async Non-zero to enable, zero to disable + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_set_async_logging(telemetry_config config, int async); + +/** + * @brief Enable or disable metrics in configuration + * @param config The configuration + * @param enabled Non-zero to enable, zero to disable + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_set_metrics_enabled(telemetry_config config, int enabled); + +/** + * @brief Enable or disable telemetry in configuration + * @param config The configuration + * @param enabled Non-zero to enable, zero to disable + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_set_enabled(telemetry_config config, int enabled); + +/** + * @brief Get the global telemetry configuration + * @return Pointer to the global configuration + */ +TELEMETRY_API telemetry_config telemetry_config_get_global(void); + +/** + * @brief Set the global telemetry configuration + * @param config The configuration to set as global + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_config_set_global(telemetry_config config); + +#ifdef __cplusplus +} +#endif + +#endif /* TELEMETRY_CONFIG_H */ diff --git a/source/telemetry/include/telemetry/telemetry_log.h b/source/telemetry/include/telemetry/telemetry_log.h new file mode 100644 index 000000000..6898b3b17 --- /dev/null +++ b/source/telemetry/include/telemetry/telemetry_log.h @@ -0,0 +1,456 @@ +/* + * Telemetry Library by MetaCall Inc. + * A library for logging and telemetry collection in MetaCall runtime. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#ifndef TELEMETRY_LOG_H +#define TELEMETRY_LOG_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +/* -- Headers -- */ + +#include + +#include +#include +#include +#include + +/* -- Definitions -- */ + +#define TELEMETRY_LOG_MAX_MESSAGE_SIZE 4096 +#define TELEMETRY_LOG_MAX_HANDLERS 16 +#define TELEMETRY_LOG_MAX_FILTERS 32 +#define TELEMETRY_LOG_TIMESTAMP_SIZE 64 +#define TELEMETRY_LOG_CATEGORY_SIZE 128 + +/* -- Type Definitions -- */ + +/** + * @brief Log severity levels + */ +typedef enum telemetry_log_level_id +{ + TELEMETRY_LOG_LEVEL_TRACE = 0, /**< Trace level logging */ + TELEMETRY_LOG_LEVEL_DEBUG = 1, /**< Debug level logging */ + TELEMETRY_LOG_LEVEL_INFO = 2, /**< Information level logging */ + TELEMETRY_LOG_LEVEL_WARNING = 3, /**< Warning level logging */ + TELEMETRY_LOG_LEVEL_ERROR = 4, /**< Error level logging */ + TELEMETRY_LOG_LEVEL_CRITICAL = 5 /**< Critical level logging */ +} telemetry_log_level_id; + +/** + * @brief Log output format types + */ +typedef enum telemetry_log_format_id +{ + TELEMETRY_LOG_FORMAT_TEXT = 0, /**< Plain text format */ + TELEMETRY_LOG_FORMAT_JSON = 1, /**< JSON format */ + TELEMETRY_LOG_FORMAT_XML = 2, /**< XML format */ + TELEMETRY_LOG_FORMAT_COLORED = 3 /**< ANSI colored text format */ +} telemetry_log_format_id; + +/** + * @brief Log handler types + */ +typedef enum telemetry_log_handler_type_id +{ + TELEMETRY_LOG_HANDLER_CONSOLE = 0, /**< Console output handler */ + TELEMETRY_LOG_HANDLER_FILE = 1, /**< File output handler */ + TELEMETRY_LOG_HANDLER_SYSLOG = 2, /**< System log handler */ + TELEMETRY_LOG_HANDLER_NETWORK = 3, /**< Network/remote handler */ + TELEMETRY_LOG_HANDLER_CUSTOM = 4 /**< Custom handler */ +} telemetry_log_handler_type_id; + +/** + * @brief Log rotation policy + */ +typedef enum telemetry_log_rotation_policy_id +{ + TELEMETRY_LOG_ROTATION_NONE = 0, /**< No rotation */ + TELEMETRY_LOG_ROTATION_SIZE = 1, /**< Rotate by size */ + TELEMETRY_LOG_ROTATION_TIME = 2, /**< Rotate by time */ + TELEMETRY_LOG_ROTATION_DAILY = 3, /**< Daily rotation */ + TELEMETRY_LOG_ROTATION_WEEKLY = 4, /**< Weekly rotation */ + TELEMETRY_LOG_ROTATION_MONTHLY = 5 /**< Monthly rotation */ +} telemetry_log_rotation_policy_id; + +/** + * @brief Forward declarations + */ +typedef struct telemetry_log_entry_type *telemetry_log_entry; +typedef struct telemetry_log_handler_type *telemetry_log_handler; +typedef struct telemetry_log_filter_type *telemetry_log_filter; +typedef struct telemetry_log_context_type *telemetry_log_context; +typedef struct telemetry_log_formatter_type *telemetry_log_formatter; + +/** + * @brief Log entry structure containing all log information + */ +struct telemetry_log_entry_type +{ + telemetry_log_level_id level; /**< Log severity level */ + char message[TELEMETRY_LOG_MAX_MESSAGE_SIZE]; /**< Log message */ + char category[TELEMETRY_LOG_CATEGORY_SIZE]; /**< Log category/module */ + char timestamp[TELEMETRY_LOG_TIMESTAMP_SIZE]; /**< Timestamp string */ + time_t time; /**< Unix timestamp */ + uint64_t thread_id; /**< Thread identifier */ + uint32_t process_id; /**< Process identifier */ + const char *file; /**< Source file name */ + const char *function; /**< Source function name */ + int line; /**< Source line number */ + void *user_data; /**< User-defined data */ +}; + +/** + * @brief Callback function type for custom log handlers + * @param entry The log entry to handle + * @param user_data User-defined data passed to the handler + * @return 0 on success, non-zero on failure + */ +typedef int (*telemetry_log_handler_callback)(telemetry_log_entry entry, void *user_data); + +/** + * @brief Callback function type for log filters + * @param entry The log entry to filter + * @param user_data User-defined data passed to the filter + * @return Non-zero if the log should be processed, zero to skip + */ +typedef int (*telemetry_log_filter_callback)(telemetry_log_entry entry, void *user_data); + +/** + * @brief Callback function type for log formatters + * @param entry The log entry to format + * @param buffer Output buffer for formatted string + * @param size Size of the output buffer + * @param user_data User-defined data passed to the formatter + * @return Number of bytes written, or negative on error + */ +typedef int (*telemetry_log_formatter_callback)(telemetry_log_entry entry, char *buffer, size_t size, void *user_data); + +/** + * @brief Log handler configuration + */ +struct telemetry_log_handler_type +{ + telemetry_log_handler_type_id type; /**< Handler type */ + telemetry_log_level_id min_level; /**< Minimum log level */ + telemetry_log_handler_callback callback; /**< Handler callback */ + telemetry_log_formatter formatter; /**< Log formatter */ + void *user_data; /**< User-defined data */ + FILE *file_handle; /**< File handle for file handlers */ + char *file_path; /**< File path for file handlers */ + telemetry_log_rotation_policy_id rotation; /**< Rotation policy */ + size_t max_file_size; /**< Max file size for rotation */ + int enabled; /**< Handler enabled flag */ +}; + +/** + * @brief Log filter configuration + */ +struct telemetry_log_filter_type +{ + telemetry_log_filter_callback callback; /**< Filter callback */ + void *user_data; /**< User-defined data */ + char category_pattern[TELEMETRY_LOG_CATEGORY_SIZE]; /**< Category pattern */ + telemetry_log_level_id min_level; /**< Minimum level */ + int enabled; /**< Filter enabled flag */ +}; + +/** + * @brief Log formatter configuration + */ +struct telemetry_log_formatter_type +{ + telemetry_log_format_id format; /**< Format type */ + telemetry_log_formatter_callback callback; /**< Custom formatter callback */ + void *user_data; /**< User-defined data */ + int include_timestamp; /**< Include timestamp flag */ + int include_level; /**< Include level flag */ + int include_category; /**< Include category flag */ + int include_location; /**< Include file/line flag */ + int include_thread_info; /**< Include thread info flag */ +}; + +/** + * @brief Main logging context + */ +struct telemetry_log_context_type +{ + telemetry_log_handler handlers[TELEMETRY_LOG_MAX_HANDLERS]; /**< Registered handlers */ + size_t handler_count; /**< Number of handlers */ + telemetry_log_filter filters[TELEMETRY_LOG_MAX_FILTERS]; /**< Registered filters */ + size_t filter_count; /**< Number of filters */ + telemetry_log_level_id global_level; /**< Global log level */ + int async_logging; /**< Async logging flag */ + void *async_queue; /**< Async log queue */ + void *mutex; /**< Thread safety mutex */ +}; + +/* -- Methods -- */ + +/** + * @brief Initialize the telemetry logging system + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_initialize(void); + +/** + * @brief Shutdown the telemetry logging system + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_shutdown(void); + +/** + * @brief Get the global logging context + * @return Pointer to the global logging context + */ +TELEMETRY_API telemetry_log_context telemetry_log_get_context(void); + +/** + * @brief Set the global log level + * @param level The minimum log level to process + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_set_level(telemetry_log_level_id level); + +/** + * @brief Get the global log level + * @return The current global log level + */ +TELEMETRY_API telemetry_log_level_id telemetry_log_get_level(void); + +/** + * @brief Create a new log handler + * @param type The type of handler to create + * @param min_level The minimum log level for this handler + * @return Pointer to the created handler, or NULL on failure + */ +TELEMETRY_API telemetry_log_handler telemetry_log_handler_create(telemetry_log_handler_type_id type, telemetry_log_level_id min_level); + +/** + * @brief Destroy a log handler + * @param handler The handler to destroy + */ +TELEMETRY_API void telemetry_log_handler_destroy(telemetry_log_handler handler); + +/** + * @brief Register a log handler with the logging system + * @param handler The handler to register + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_handler_register(telemetry_log_handler handler); + +/** + * @brief Unregister a log handler from the logging system + * @param handler The handler to unregister + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_handler_unregister(telemetry_log_handler handler); + +/** + * @brief Set a custom callback for a log handler + * @param handler The handler to configure + * @param callback The callback function + * @param user_data User-defined data to pass to the callback + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_handler_set_callback(telemetry_log_handler handler, telemetry_log_handler_callback callback, void *user_data); + +/** + * @brief Configure file handler settings + * @param handler The file handler to configure + * @param file_path Path to the log file + * @param rotation Rotation policy + * @param max_size Maximum file size for rotation (bytes) + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_handler_configure_file(telemetry_log_handler handler, const char *file_path, telemetry_log_rotation_policy_id rotation, size_t max_size); + +/** + * @brief Create a new log formatter + * @param format The format type + * @return Pointer to the created formatter, or NULL on failure + */ +TELEMETRY_API telemetry_log_formatter telemetry_log_formatter_create(telemetry_log_format_id format); + +/** + * @brief Destroy a log formatter + * @param formatter The formatter to destroy + */ +TELEMETRY_API void telemetry_log_formatter_destroy(telemetry_log_formatter formatter); + +/** + * @brief Set formatter options + * @param formatter The formatter to configure + * @param include_timestamp Include timestamp in output + * @param include_level Include log level in output + * @param include_category Include category in output + * @param include_location Include file/line in output + * @param include_thread_info Include thread info in output + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_formatter_configure(telemetry_log_formatter formatter, int include_timestamp, int include_level, int include_category, int include_location, int include_thread_info); + +/** + * @brief Set a custom formatter callback + * @param formatter The formatter to configure + * @param callback The callback function + * @param user_data User-defined data to pass to the callback + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_formatter_set_callback(telemetry_log_formatter formatter, telemetry_log_formatter_callback callback, void *user_data); + +/** + * @brief Attach a formatter to a handler + * @param handler The handler to attach the formatter to + * @param formatter The formatter to attach + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_handler_set_formatter(telemetry_log_handler handler, telemetry_log_formatter formatter); + +/** + * @brief Create a new log filter + * @return Pointer to the created filter, or NULL on failure + */ +TELEMETRY_API telemetry_log_filter telemetry_log_filter_create(void); + +/** + * @brief Destroy a log filter + * @param filter The filter to destroy + */ +TELEMETRY_API void telemetry_log_filter_destroy(telemetry_log_filter filter); + +/** + * @brief Register a log filter with the logging system + * @param filter The filter to register + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_filter_register(telemetry_log_filter filter); + +/** + * @brief Unregister a log filter from the logging system + * @param filter The filter to unregister + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_filter_unregister(telemetry_log_filter filter); + +/** + * @brief Set a custom callback for a log filter + * @param filter The filter to configure + * @param callback The callback function + * @param user_data User-defined data to pass to the callback + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_filter_set_callback(telemetry_log_filter filter, telemetry_log_filter_callback callback, void *user_data); + +/** + * @brief Set filter category pattern + * @param filter The filter to configure + * @param pattern Category pattern (supports wildcards) + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_filter_set_category(telemetry_log_filter filter, const char *pattern); + +/** + * @brief Set filter minimum level + * @param filter The filter to configure + * @param level Minimum log level + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_filter_set_level(telemetry_log_filter filter, telemetry_log_level_id level); + +/** + * @brief Log a message with full context + * @param level Log severity level + * @param category Log category/module + * @param file Source file name + * @param function Source function name + * @param line Source line number + * @param format Printf-style format string + * @param ... Format arguments + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_write(telemetry_log_level_id level, const char *category, const char *file, const char *function, int line, const char *format, ...); + +/** + * @brief Log a message with a pre-formatted entry + * @param entry The log entry to write + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_write_entry(telemetry_log_entry entry); + +/** + * @brief Enable or disable asynchronous logging + * @param async Non-zero to enable, zero to disable + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_set_async(int async); + +/** + * @brief Flush all pending log entries + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_log_flush(void); + +/** + * @brief Get the string representation of a log level + * @param level The log level + * @return String representation of the level + */ +TELEMETRY_API const char *telemetry_log_level_to_string(telemetry_log_level_id level); + +/** + * @brief Parse a log level from a string + * @param str String representation of the level + * @return The log level, or TELEMETRY_LOG_LEVEL_INFO if invalid + */ +TELEMETRY_API telemetry_log_level_id telemetry_log_level_from_string(const char *str); + +/* -- Convenience Macros -- */ + +#define TELEMETRY_LOG(level, category, ...) \ + telemetry_log_write(level, category, __FILE__, __FUNCTION__, __LINE__, __VA_ARGS__) + +#define TELEMETRY_LOG_TRACE(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_TRACE, category, __VA_ARGS__) + +#define TELEMETRY_LOG_DEBUG(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_DEBUG, category, __VA_ARGS__) + +#define TELEMETRY_LOG_INFO(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_INFO, category, __VA_ARGS__) + +#define TELEMETRY_LOG_WARNING(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_WARNING, category, __VA_ARGS__) + +#define TELEMETRY_LOG_ERROR(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_ERROR, category, __VA_ARGS__) + +#define TELEMETRY_LOG_CRITICAL(category, ...) \ + TELEMETRY_LOG(TELEMETRY_LOG_LEVEL_CRITICAL, category, __VA_ARGS__) + +#ifdef __cplusplus +} +#endif + +#endif /* TELEMETRY_LOG_H */ diff --git a/source/telemetry/include/telemetry/telemetry_metrics.h b/source/telemetry/include/telemetry/telemetry_metrics.h new file mode 100644 index 000000000..881896284 --- /dev/null +++ b/source/telemetry/include/telemetry/telemetry_metrics.h @@ -0,0 +1,494 @@ +/* + * Telemetry Library by MetaCall Inc. + * A library for logging and telemetry collection in MetaCall runtime. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#ifndef TELEMETRY_METRICS_H +#define TELEMETRY_METRICS_H 1 + +#ifdef __cplusplus +extern "C" { +#endif + +/* -- Headers -- */ + +#include + +#include +#include +#include + +/* -- Definitions -- */ + +#define TELEMETRY_METRICS_MAX_NAME_SIZE 256 +#define TELEMETRY_METRICS_MAX_LABELS 16 +#define TELEMETRY_METRICS_MAX_LABEL_SIZE 128 +#define TELEMETRY_METRICS_MAX_METRICS 1024 +#define TELEMETRY_METRICS_MAX_EXPORTERS 8 +#define TELEMETRY_METRICS_HISTOGRAM_BUCKETS 20 + +/* -- Type Definitions -- */ + +/** + * @brief Metric types + */ +typedef enum telemetry_metric_type_id +{ + TELEMETRY_METRIC_TYPE_COUNTER = 0, /**< Monotonically increasing counter */ + TELEMETRY_METRIC_TYPE_GAUGE = 1, /**< Value that can go up and down */ + TELEMETRY_METRIC_TYPE_HISTOGRAM = 2, /**< Statistical distribution */ + TELEMETRY_METRIC_TYPE_SUMMARY = 3 /**< Similar to histogram with quantiles */ +} telemetry_metric_type_id; + +/** + * @brief Metric aggregation types + */ +typedef enum telemetry_metric_aggregation_id +{ + TELEMETRY_METRIC_AGGREGATION_SUM = 0, /**< Sum of values */ + TELEMETRY_METRIC_AGGREGATION_AVG = 1, /**< Average of values */ + TELEMETRY_METRIC_AGGREGATION_MIN = 2, /**< Minimum value */ + TELEMETRY_METRIC_AGGREGATION_MAX = 3, /**< Maximum value */ + TELEMETRY_METRIC_AGGREGATION_COUNT = 4, /**< Count of values */ + TELEMETRY_METRIC_AGGREGATION_RATE = 5 /**< Rate of change */ +} telemetry_metric_aggregation_id; + +/** + * @brief Export format types + */ +typedef enum telemetry_metric_export_format_id +{ + TELEMETRY_METRIC_EXPORT_PROMETHEUS = 0, /**< Prometheus text format */ + TELEMETRY_METRIC_EXPORT_JSON = 1, /**< JSON format */ + TELEMETRY_METRIC_EXPORT_STATSD = 2, /**< StatsD format */ + TELEMETRY_METRIC_EXPORT_INFLUXDB = 3, /**< InfluxDB line protocol */ + TELEMETRY_METRIC_EXPORT_GRAPHITE = 4 /**< Graphite plaintext format */ +} telemetry_metric_export_format_id; + +/** + * @brief Forward declarations + */ +typedef struct telemetry_metric_label_type *telemetry_metric_label; +typedef struct telemetry_metric_type *telemetry_metric; +typedef struct telemetry_metric_counter_type *telemetry_metric_counter; +typedef struct telemetry_metric_gauge_type *telemetry_metric_gauge; +typedef struct telemetry_metric_histogram_type *telemetry_metric_histogram; +typedef struct telemetry_metric_registry_type *telemetry_metric_registry; +typedef struct telemetry_metric_exporter_type *telemetry_metric_exporter; + +/** + * @brief Label for metric dimensions + */ +struct telemetry_metric_label_type +{ + char key[TELEMETRY_METRICS_MAX_LABEL_SIZE]; /**< Label key */ + char value[TELEMETRY_METRICS_MAX_LABEL_SIZE]; /**< Label value */ +}; + +/** + * @brief Histogram bucket + */ +struct telemetry_histogram_bucket_type +{ + double upper_bound; /**< Upper bound of the bucket */ + uint64_t count; /**< Count of observations in bucket */ +}; + +/** + * @brief Counter metric + */ +struct telemetry_metric_counter_type +{ + uint64_t value; /**< Current counter value */ + time_t last_updated; /**< Last update timestamp */ + uint64_t increment_count; /**< Number of increments */ +}; + +/** + * @brief Gauge metric + */ +struct telemetry_metric_gauge_type +{ + double value; /**< Current gauge value */ + double min_value; /**< Minimum observed value */ + double max_value; /**< Maximum observed value */ + time_t last_updated; /**< Last update timestamp */ +}; + +/** + * @brief Histogram metric + */ +struct telemetry_metric_histogram_type +{ + struct telemetry_histogram_bucket_type buckets[TELEMETRY_METRICS_HISTOGRAM_BUCKETS]; /**< Histogram buckets */ + size_t bucket_count; /**< Number of buckets */ + uint64_t total_count; /**< Total number of observations */ + double sum; /**< Sum of all observations */ + double min; /**< Minimum observation */ + double max; /**< Maximum observation */ +}; + +/** + * @brief Generic metric structure + */ +struct telemetry_metric_type +{ + char name[TELEMETRY_METRICS_MAX_NAME_SIZE]; /**< Metric name */ + char description[TELEMETRY_METRICS_MAX_NAME_SIZE]; /**< Metric description */ + char unit[64]; /**< Unit of measurement */ + telemetry_metric_type_id type; /**< Metric type */ + struct telemetry_metric_label_type labels[TELEMETRY_METRICS_MAX_LABELS]; /**< Metric labels */ + size_t label_count; /**< Number of labels */ + time_t created_at; /**< Creation timestamp */ + int enabled; /**< Enabled flag */ + + union + { + struct telemetry_metric_counter_type counter; /**< Counter data */ + struct telemetry_metric_gauge_type gauge; /**< Gauge data */ + struct telemetry_metric_histogram_type histogram; /**< Histogram data */ + } data; +}; + +/** + * @brief Callback function for custom metric exporters + * @param metric The metric to export + * @param buffer Output buffer + * @param size Buffer size + * @param user_data User-defined data + * @return Number of bytes written, or negative on error + */ +typedef int (*telemetry_metric_exporter_callback)(telemetry_metric metric, char *buffer, size_t size, void *user_data); + +/** + * @brief Metric exporter + */ +struct telemetry_metric_exporter_type +{ + telemetry_metric_export_format_id format; /**< Export format */ + telemetry_metric_exporter_callback callback; /**< Custom exporter callback */ + void *user_data; /**< User-defined data */ + char endpoint[256]; /**< Export endpoint (URL, file path, etc.) */ + int push_interval; /**< Push interval in seconds */ + time_t last_export; /**< Last export timestamp */ + int enabled; /**< Enabled flag */ +}; + +/** + * @brief Metric registry + */ +struct telemetry_metric_registry_type +{ + telemetry_metric metrics[TELEMETRY_METRICS_MAX_METRICS]; /**< Registered metrics */ + size_t metric_count; /**< Number of metrics */ + telemetry_metric_exporter exporters[TELEMETRY_METRICS_MAX_EXPORTERS]; /**< Registered exporters */ + size_t exporter_count; /**< Number of exporters */ + void *mutex; /**< Thread safety mutex */ +}; + +/* -- Methods -- */ + +/** + * @brief Initialize the telemetry metrics system + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metrics_initialize(void); + +/** + * @brief Shutdown the telemetry metrics system + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metrics_shutdown(void); + +/** + * @brief Get the global metrics registry + * @return Pointer to the global registry + */ +TELEMETRY_API telemetry_metric_registry telemetry_metrics_get_registry(void); + +/** + * @brief Create a new counter metric + * @param name Metric name + * @param description Metric description + * @param unit Unit of measurement + * @return Pointer to the created metric, or NULL on failure + */ +TELEMETRY_API telemetry_metric telemetry_metric_counter_create(const char *name, const char *description, const char *unit); + +/** + * @brief Create a new gauge metric + * @param name Metric name + * @param description Metric description + * @param unit Unit of measurement + * @return Pointer to the created metric, or NULL on failure + */ +TELEMETRY_API telemetry_metric telemetry_metric_gauge_create(const char *name, const char *description, const char *unit); + +/** + * @brief Create a new histogram metric + * @param name Metric name + * @param description Metric description + * @param unit Unit of measurement + * @param buckets Array of bucket upper bounds + * @param bucket_count Number of buckets + * @return Pointer to the created metric, or NULL on failure + */ +TELEMETRY_API telemetry_metric telemetry_metric_histogram_create(const char *name, const char *description, const char *unit, const double *buckets, size_t bucket_count); + +/** + * @brief Register a metric with the global registry + * @param metric The metric to register + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_register(telemetry_metric metric); + +/** + * @brief Unregister a metric from the global registry + * @param metric The metric to unregister + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_unregister(telemetry_metric metric); + +/** + * @brief Find a metric by name + * @param name The metric name + * @return Pointer to the metric, or NULL if not found + */ +TELEMETRY_API telemetry_metric telemetry_metric_find(const char *name); + +/** + * @brief Destroy a metric + * @param metric The metric to destroy + */ +TELEMETRY_API void telemetry_metric_destroy(telemetry_metric metric); + +/** + * @brief Add a label to a metric + * @param metric The metric + * @param key Label key + * @param value Label value + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_add_label(telemetry_metric metric, const char *key, const char *value); + +/** + * @brief Increment a counter metric + * @param metric The counter metric + * @param value Value to increment by (default 1) + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_counter_increment(telemetry_metric metric, uint64_t value); + +/** + * @brief Set a gauge metric value + * @param metric The gauge metric + * @param value New value + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_gauge_set(telemetry_metric metric, double value); + +/** + * @brief Increment a gauge metric + * @param metric The gauge metric + * @param value Value to increment by + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_gauge_increment(telemetry_metric metric, double value); + +/** + * @brief Decrement a gauge metric + * @param metric The gauge metric + * @param value Value to decrement by + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_gauge_decrement(telemetry_metric metric, double value); + +/** + * @brief Observe a value in a histogram + * @param metric The histogram metric + * @param value Observed value + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_histogram_observe(telemetry_metric metric, double value); + +/** + * @brief Get counter value + * @param metric The counter metric + * @return Current counter value, or 0 on error + */ +TELEMETRY_API uint64_t telemetry_metric_counter_get(telemetry_metric metric); + +/** + * @brief Get gauge value + * @param metric The gauge metric + * @return Current gauge value, or 0.0 on error + */ +TELEMETRY_API double telemetry_metric_gauge_get(telemetry_metric metric); + +/** + * @brief Get histogram statistics + * @param metric The histogram metric + * @param count Output: total count + * @param sum Output: sum of observations + * @param min Output: minimum value + * @param max Output: maximum value + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_histogram_get_stats(telemetry_metric metric, uint64_t *count, double *sum, double *min, double *max); + +/** + * @brief Create a metric exporter + * @param format Export format + * @param endpoint Export endpoint + * @param push_interval Push interval in seconds (0 for manual export) + * @return Pointer to the created exporter, or NULL on failure + */ +TELEMETRY_API telemetry_metric_exporter telemetry_metric_exporter_create(telemetry_metric_export_format_id format, const char *endpoint, int push_interval); + +/** + * @brief Destroy a metric exporter + * @param exporter The exporter to destroy + */ +TELEMETRY_API void telemetry_metric_exporter_destroy(telemetry_metric_exporter exporter); + +/** + * @brief Register a metric exporter + * @param exporter The exporter to register + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_exporter_register(telemetry_metric_exporter exporter); + +/** + * @brief Unregister a metric exporter + * @param exporter The exporter to unregister + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_exporter_unregister(telemetry_metric_exporter exporter); + +/** + * @brief Set custom exporter callback + * @param exporter The exporter + * @param callback Custom callback function + * @param user_data User-defined data + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_exporter_set_callback(telemetry_metric_exporter exporter, telemetry_metric_exporter_callback callback, void *user_data); + +/** + * @brief Export all metrics using all registered exporters + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metrics_export_all(void); + +/** + * @brief Export metrics to a buffer using specified format + * @param format Export format + * @param buffer Output buffer + * @param size Buffer size + * @return Number of bytes written, or negative on error + */ +TELEMETRY_API int telemetry_metrics_export_to_buffer(telemetry_metric_export_format_id format, char *buffer, size_t size); + +/** + * @brief Export a single metric to a buffer + * @param metric The metric to export + * @param format Export format + * @param buffer Output buffer + * @param size Buffer size + * @return Number of bytes written, or negative on error + */ +TELEMETRY_API int telemetry_metric_export(telemetry_metric metric, telemetry_metric_export_format_id format, char *buffer, size_t size); + +/** + * @brief Reset a counter metric to zero + * @param metric The counter metric + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_counter_reset(telemetry_metric metric); + +/** + * @brief Reset a histogram metric + * @param metric The histogram metric + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metric_histogram_reset(telemetry_metric metric); + +/** + * @brief Reset all metrics in the registry + * @return 0 on success, non-zero on failure + */ +TELEMETRY_API int telemetry_metrics_reset_all(void); + +/** + * @brief Get the number of registered metrics + * @return Number of metrics + */ +TELEMETRY_API size_t telemetry_metrics_get_count(void); + +/** + * @brief Calculate aggregation over a metric (for gauges and histograms) + * @param metric The metric + * @param aggregation Aggregation type + * @return Aggregated value + */ +TELEMETRY_API double telemetry_metric_aggregate(telemetry_metric metric, telemetry_metric_aggregation_id aggregation); + +/* -- Convenience Macros -- */ + +#define TELEMETRY_COUNTER_INC(name) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_counter_increment(m, 1); \ + } while(0) + +#define TELEMETRY_COUNTER_ADD(name, value) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_counter_increment(m, value); \ + } while(0) + +#define TELEMETRY_GAUGE_SET(name, value) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_gauge_set(m, value); \ + } while(0) + +#define TELEMETRY_GAUGE_INC(name, value) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_gauge_increment(m, value); \ + } while(0) + +#define TELEMETRY_GAUGE_DEC(name, value) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_gauge_decrement(m, value); \ + } while(0) + +#define TELEMETRY_HISTOGRAM_OBSERVE(name, value) \ + do { \ + telemetry_metric m = telemetry_metric_find(name); \ + if (m != NULL) telemetry_metric_histogram_observe(m, value); \ + } while(0) + +#ifdef __cplusplus +} +#endif + +#endif /* TELEMETRY_METRICS_H */ diff --git a/source/telemetry/source/telemetry_log.c b/source/telemetry/source/telemetry_log.c new file mode 100644 index 000000000..9a1ab576d --- /dev/null +++ b/source/telemetry/source/telemetry_log.c @@ -0,0 +1,1217 @@ +/* + * Telemetry Library by MetaCall Inc. + * A library for logging and telemetry collection in MetaCall runtime. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* -- Headers -- */ + +#include + +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) + #include + #include + #define getpid _getpid + #define snprintf _snprintf +#else + #include + #include + #include + #include +#endif + +/* -- Private Data -- */ + +static struct telemetry_log_context_type global_context = { + .handler_count = 0, + .filter_count = 0, + .global_level = TELEMETRY_LOG_LEVEL_INFO, + .async_logging = 0, + .async_queue = NULL, + .mutex = NULL +}; + +static int telemetry_log_initialized = 0; + +/* -- Private Forward Declarations -- */ + +static int telemetry_log_entry_format_text(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter); +static int telemetry_log_entry_format_json(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter); +static int telemetry_log_entry_format_xml(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter); +static int telemetry_log_entry_format_colored(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter); +static int telemetry_log_handler_process(telemetry_log_handler handler, telemetry_log_entry entry); +static int telemetry_log_filter_match(telemetry_log_filter filter, telemetry_log_entry entry); +static void telemetry_log_get_timestamp(char *buffer, size_t size); +static uint64_t telemetry_log_get_thread_id(void); +static const char *telemetry_log_get_color_code(telemetry_log_level_id level); +static const char *telemetry_log_get_reset_code(void); +static int telemetry_log_rotate_file(telemetry_log_handler handler); +static int telemetry_log_should_rotate(telemetry_log_handler handler); + +/* -- Methods -- */ + +int telemetry_log_initialize(void) +{ + if (telemetry_log_initialized) + { + return 0; + } + + /* Initialize the global context */ + memset(&global_context, 0, sizeof(struct telemetry_log_context_type)); + global_context.global_level = TELEMETRY_LOG_LEVEL_INFO; + global_context.async_logging = 0; + + /* TODO: Initialize mutex for thread safety */ + /* TODO: Initialize async queue if needed */ + + telemetry_log_initialized = 1; + + return 0; +} + +int telemetry_log_shutdown(void) +{ + size_t i; + + if (!telemetry_log_initialized) + { + return 0; + } + + /* Flush all pending logs */ + telemetry_log_flush(); + + /* Destroy all handlers */ + for (i = 0; i < global_context.handler_count; ++i) + { + if (global_context.handlers[i] != NULL) + { + telemetry_log_handler_destroy(global_context.handlers[i]); + global_context.handlers[i] = NULL; + } + } + + /* Destroy all filters */ + for (i = 0; i < global_context.filter_count; ++i) + { + if (global_context.filters[i] != NULL) + { + telemetry_log_filter_destroy(global_context.filters[i]); + global_context.filters[i] = NULL; + } + } + + /* TODO: Destroy mutex */ + /* TODO: Destroy async queue */ + + telemetry_log_initialized = 0; + + return 0; +} + +telemetry_log_context telemetry_log_get_context(void) +{ + return &global_context; +} + +int telemetry_log_set_level(telemetry_log_level_id level) +{ + if (level < TELEMETRY_LOG_LEVEL_TRACE || level > TELEMETRY_LOG_LEVEL_CRITICAL) + { + return 1; + } + + global_context.global_level = level; + return 0; +} + +telemetry_log_level_id telemetry_log_get_level(void) +{ + return global_context.global_level; +} + +telemetry_log_handler telemetry_log_handler_create(telemetry_log_handler_type_id type, telemetry_log_level_id min_level) +{ + telemetry_log_handler handler = (telemetry_log_handler)malloc(sizeof(struct telemetry_log_handler_type)); + + if (handler == NULL) + { + return NULL; + } + + memset(handler, 0, sizeof(struct telemetry_log_handler_type)); + + handler->type = type; + handler->min_level = min_level; + handler->callback = NULL; + handler->formatter = NULL; + handler->user_data = NULL; + handler->file_handle = NULL; + handler->file_path = NULL; + handler->rotation = TELEMETRY_LOG_ROTATION_NONE; + handler->max_file_size = 0; + handler->enabled = 1; + + return handler; +} + +void telemetry_log_handler_destroy(telemetry_log_handler handler) +{ + if (handler == NULL) + { + return; + } + + /* Close file handle if open */ + if (handler->file_handle != NULL && handler->file_handle != stdout && handler->file_handle != stderr) + { + fclose(handler->file_handle); + } + + /* Free file path */ + if (handler->file_path != NULL) + { + free(handler->file_path); + } + + /* Destroy formatter if present */ + if (handler->formatter != NULL) + { + telemetry_log_formatter_destroy(handler->formatter); + } + + free(handler); +} + +int telemetry_log_handler_register(telemetry_log_handler handler) +{ + if (handler == NULL) + { + return 1; + } + + if (global_context.handler_count >= TELEMETRY_LOG_MAX_HANDLERS) + { + return 1; + } + + global_context.handlers[global_context.handler_count++] = handler; + + return 0; +} + +int telemetry_log_handler_unregister(telemetry_log_handler handler) +{ + size_t i, j; + + if (handler == NULL) + { + return 1; + } + + for (i = 0; i < global_context.handler_count; ++i) + { + if (global_context.handlers[i] == handler) + { + /* Shift remaining handlers */ + for (j = i; j < global_context.handler_count - 1; ++j) + { + global_context.handlers[j] = global_context.handlers[j + 1]; + } + + global_context.handler_count--; + return 0; + } + } + + return 1; +} + +int telemetry_log_handler_set_callback(telemetry_log_handler handler, telemetry_log_handler_callback callback, void *user_data) +{ + if (handler == NULL || callback == NULL) + { + return 1; + } + + handler->callback = callback; + handler->user_data = user_data; + + return 0; +} + +int telemetry_log_handler_configure_file(telemetry_log_handler handler, const char *file_path, telemetry_log_rotation_policy_id rotation, size_t max_size) +{ + if (handler == NULL || file_path == NULL) + { + return 1; + } + + if (handler->type != TELEMETRY_LOG_HANDLER_FILE) + { + return 1; + } + + /* Close existing file if open */ + if (handler->file_handle != NULL) + { + fclose(handler->file_handle); + handler->file_handle = NULL; + } + + /* Free existing path */ + if (handler->file_path != NULL) + { + free(handler->file_path); + } + + /* Allocate and copy new path */ + handler->file_path = (char *)malloc(strlen(file_path) + 1); + if (handler->file_path == NULL) + { + return 1; + } + strcpy(handler->file_path, file_path); + + /* Open the file */ + handler->file_handle = fopen(file_path, "a"); + if (handler->file_handle == NULL) + { + free(handler->file_path); + handler->file_path = NULL; + return 1; + } + + handler->rotation = rotation; + handler->max_file_size = max_size; + + return 0; +} + +telemetry_log_formatter telemetry_log_formatter_create(telemetry_log_format_id format) +{ + telemetry_log_formatter formatter = (telemetry_log_formatter)malloc(sizeof(struct telemetry_log_formatter_type)); + + if (formatter == NULL) + { + return NULL; + } + + memset(formatter, 0, sizeof(struct telemetry_log_formatter_type)); + + formatter->format = format; + formatter->callback = NULL; + formatter->user_data = NULL; + formatter->include_timestamp = 1; + formatter->include_level = 1; + formatter->include_category = 1; + formatter->include_location = 1; + formatter->include_thread_info = 0; + + return formatter; +} + +void telemetry_log_formatter_destroy(telemetry_log_formatter formatter) +{ + if (formatter == NULL) + { + return; + } + + free(formatter); +} + +int telemetry_log_formatter_configure(telemetry_log_formatter formatter, int include_timestamp, int include_level, int include_category, int include_location, int include_thread_info) +{ + if (formatter == NULL) + { + return 1; + } + + formatter->include_timestamp = include_timestamp; + formatter->include_level = include_level; + formatter->include_category = include_category; + formatter->include_location = include_location; + formatter->include_thread_info = include_thread_info; + + return 0; +} + +int telemetry_log_formatter_set_callback(telemetry_log_formatter formatter, telemetry_log_formatter_callback callback, void *user_data) +{ + if (formatter == NULL || callback == NULL) + { + return 1; + } + + formatter->callback = callback; + formatter->user_data = user_data; + + return 0; +} + +int telemetry_log_handler_set_formatter(telemetry_log_handler handler, telemetry_log_formatter formatter) +{ + if (handler == NULL || formatter == NULL) + { + return 1; + } + + handler->formatter = formatter; + + return 0; +} + +telemetry_log_filter telemetry_log_filter_create(void) +{ + telemetry_log_filter filter = (telemetry_log_filter)malloc(sizeof(struct telemetry_log_filter_type)); + + if (filter == NULL) + { + return NULL; + } + + memset(filter, 0, sizeof(struct telemetry_log_filter_type)); + + filter->callback = NULL; + filter->user_data = NULL; + filter->category_pattern[0] = '\0'; + filter->min_level = TELEMETRY_LOG_LEVEL_TRACE; + filter->enabled = 1; + + return filter; +} + +void telemetry_log_filter_destroy(telemetry_log_filter filter) +{ + if (filter == NULL) + { + return; + } + + free(filter); +} + +int telemetry_log_filter_register(telemetry_log_filter filter) +{ + if (filter == NULL) + { + return 1; + } + + if (global_context.filter_count >= TELEMETRY_LOG_MAX_FILTERS) + { + return 1; + } + + global_context.filters[global_context.filter_count++] = filter; + + return 0; +} + +int telemetry_log_filter_unregister(telemetry_log_filter filter) +{ + size_t i, j; + + if (filter == NULL) + { + return 1; + } + + for (i = 0; i < global_context.filter_count; ++i) + { + if (global_context.filters[i] == filter) + { + /* Shift remaining filters */ + for (j = i; j < global_context.filter_count - 1; ++j) + { + global_context.filters[j] = global_context.filters[j + 1]; + } + + global_context.filter_count--; + return 0; + } + } + + return 1; +} + +int telemetry_log_filter_set_callback(telemetry_log_filter filter, telemetry_log_filter_callback callback, void *user_data) +{ + if (filter == NULL || callback == NULL) + { + return 1; + } + + filter->callback = callback; + filter->user_data = user_data; + + return 0; +} + +int telemetry_log_filter_set_category(telemetry_log_filter filter, const char *pattern) +{ + if (filter == NULL || pattern == NULL) + { + return 1; + } + + strncpy(filter->category_pattern, pattern, TELEMETRY_LOG_CATEGORY_SIZE - 1); + filter->category_pattern[TELEMETRY_LOG_CATEGORY_SIZE - 1] = '\0'; + + return 0; +} + +int telemetry_log_filter_set_level(telemetry_log_filter filter, telemetry_log_level_id level) +{ + if (filter == NULL) + { + return 1; + } + + if (level < TELEMETRY_LOG_LEVEL_TRACE || level > TELEMETRY_LOG_LEVEL_CRITICAL) + { + return 1; + } + + filter->min_level = level; + + return 0; +} + +int telemetry_log_write(telemetry_log_level_id level, const char *category, const char *file, const char *function, int line, const char *format, ...) +{ + struct telemetry_log_entry_type entry; + va_list args; + size_t i; + int should_log = 0; + + /* Check if logging is initialized */ + if (!telemetry_log_initialized) + { + telemetry_log_initialize(); + } + + /* Check global log level */ + if (level < global_context.global_level) + { + return 0; + } + + /* Initialize entry */ + memset(&entry, 0, sizeof(struct telemetry_log_entry_type)); + entry.level = level; + entry.file = file; + entry.function = function; + entry.line = line; + entry.time = time(NULL); + entry.thread_id = telemetry_log_get_thread_id(); + entry.process_id = (uint32_t)getpid(); + + /* Copy category */ + if (category != NULL) + { + strncpy(entry.category, category, TELEMETRY_LOG_CATEGORY_SIZE - 1); + entry.category[TELEMETRY_LOG_CATEGORY_SIZE - 1] = '\0'; + } + else + { + strcpy(entry.category, "general"); + } + + /* Format message */ + va_start(args, format); + vsnprintf(entry.message, TELEMETRY_LOG_MAX_MESSAGE_SIZE, format, args); + va_end(args); + + /* Generate timestamp */ + telemetry_log_get_timestamp(entry.timestamp, TELEMETRY_LOG_TIMESTAMP_SIZE); + + /* Apply filters */ + should_log = 1; + for (i = 0; i < global_context.filter_count; ++i) + { + if (global_context.filters[i] != NULL && global_context.filters[i]->enabled) + { + if (!telemetry_log_filter_match(global_context.filters[i], &entry)) + { + should_log = 0; + break; + } + } + } + + if (!should_log) + { + return 0; + } + + /* Process with all handlers */ + return telemetry_log_write_entry(&entry); +} + +int telemetry_log_write_entry(telemetry_log_entry entry) +{ + size_t i; + int result = 0; + + if (entry == NULL) + { + return 1; + } + + /* TODO: If async logging is enabled, queue the entry */ + if (global_context.async_logging) + { + /* Queue entry for async processing */ + /* For now, fall through to synchronous processing */ + } + + /* Process with all registered handlers */ + for (i = 0; i < global_context.handler_count; ++i) + { + if (global_context.handlers[i] != NULL && global_context.handlers[i]->enabled) + { + if (entry->level >= global_context.handlers[i]->min_level) + { + int handler_result = telemetry_log_handler_process(global_context.handlers[i], entry); + if (handler_result != 0) + { + result = handler_result; + } + } + } + } + + return result; +} + +int telemetry_log_set_async(int async) +{ + global_context.async_logging = async; + + /* TODO: Initialize or destroy async queue based on flag */ + + return 0; +} + +int telemetry_log_flush(void) +{ + size_t i; + + /* TODO: Flush async queue if present */ + + /* Flush all file handlers */ + for (i = 0; i < global_context.handler_count; ++i) + { + if (global_context.handlers[i] != NULL) + { + telemetry_log_handler handler = global_context.handlers[i]; + if (handler->file_handle != NULL) + { + fflush(handler->file_handle); + } + } + } + + return 0; +} + +const char *telemetry_log_level_to_string(telemetry_log_level_id level) +{ + switch (level) + { + case TELEMETRY_LOG_LEVEL_TRACE: + return "TRACE"; + case TELEMETRY_LOG_LEVEL_DEBUG: + return "DEBUG"; + case TELEMETRY_LOG_LEVEL_INFO: + return "INFO"; + case TELEMETRY_LOG_LEVEL_WARNING: + return "WARNING"; + case TELEMETRY_LOG_LEVEL_ERROR: + return "ERROR"; + case TELEMETRY_LOG_LEVEL_CRITICAL: + return "CRITICAL"; + default: + return "UNKNOWN"; + } +} + +telemetry_log_level_id telemetry_log_level_from_string(const char *str) +{ + if (str == NULL) + { + return TELEMETRY_LOG_LEVEL_INFO; + } + + if (strcmp(str, "TRACE") == 0 || strcmp(str, "trace") == 0) + { + return TELEMETRY_LOG_LEVEL_TRACE; + } + else if (strcmp(str, "DEBUG") == 0 || strcmp(str, "debug") == 0) + { + return TELEMETRY_LOG_LEVEL_DEBUG; + } + else if (strcmp(str, "INFO") == 0 || strcmp(str, "info") == 0) + { + return TELEMETRY_LOG_LEVEL_INFO; + } + else if (strcmp(str, "WARNING") == 0 || strcmp(str, "warning") == 0 || strcmp(str, "WARN") == 0 || strcmp(str, "warn") == 0) + { + return TELEMETRY_LOG_LEVEL_WARNING; + } + else if (strcmp(str, "ERROR") == 0 || strcmp(str, "error") == 0) + { + return TELEMETRY_LOG_LEVEL_ERROR; + } + else if (strcmp(str, "CRITICAL") == 0 || strcmp(str, "critical") == 0 || strcmp(str, "CRIT") == 0 || strcmp(str, "crit") == 0) + { + return TELEMETRY_LOG_LEVEL_CRITICAL; + } + + return TELEMETRY_LOG_LEVEL_INFO; +} + +/* -- Private Methods -- */ + +static int telemetry_log_entry_format_text(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter) +{ + int written = 0; + int n; + + if (entry == NULL || buffer == NULL || size == 0) + { + return -1; + } + + buffer[0] = '\0'; + + /* Timestamp */ + if (formatter->include_timestamp) + { + n = snprintf(buffer + written, size - written, "[%s] ", entry->timestamp); + if (n > 0) + written += n; + } + + /* Log level */ + if (formatter->include_level) + { + n = snprintf(buffer + written, size - written, "[%s] ", telemetry_log_level_to_string(entry->level)); + if (n > 0) + written += n; + } + + /* Category */ + if (formatter->include_category && entry->category[0] != '\0') + { + n = snprintf(buffer + written, size - written, "[%s] ", entry->category); + if (n > 0) + written += n; + } + + /* Thread info */ + if (formatter->include_thread_info) + { + n = snprintf(buffer + written, size - written, "[pid:%u tid:%llu] ", entry->process_id, (unsigned long long)entry->thread_id); + if (n > 0) + written += n; + } + + /* Message */ + n = snprintf(buffer + written, size - written, "%s", entry->message); + if (n > 0) + written += n; + + /* Location */ + if (formatter->include_location && entry->file != NULL) + { + n = snprintf(buffer + written, size - written, " (%s:%d in %s)", entry->file, entry->line, entry->function ? entry->function : "?"); + if (n > 0) + written += n; + } + + return written; +} + +static int telemetry_log_entry_format_json(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter) +{ + int written = 0; + int n; + + if (entry == NULL || buffer == NULL || size == 0) + { + return -1; + } + + n = snprintf(buffer + written, size - written, "{"); + if (n > 0) + written += n; + + if (formatter->include_timestamp) + { + n = snprintf(buffer + written, size - written, "\"timestamp\":\"%s\",", entry->timestamp); + if (n > 0) + written += n; + } + + if (formatter->include_level) + { + n = snprintf(buffer + written, size - written, "\"level\":\"%s\",", telemetry_log_level_to_string(entry->level)); + if (n > 0) + written += n; + } + + if (formatter->include_category) + { + n = snprintf(buffer + written, size - written, "\"category\":\"%s\",", entry->category); + if (n > 0) + written += n; + } + + if (formatter->include_thread_info) + { + n = snprintf(buffer + written, size - written, "\"process_id\":%u,\"thread_id\":%llu,", entry->process_id, (unsigned long long)entry->thread_id); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "\"message\":\"%s\"", entry->message); + if (n > 0) + written += n; + + if (formatter->include_location && entry->file != NULL) + { + n = snprintf(buffer + written, size - written, ",\"file\":\"%s\",\"line\":%d,\"function\":\"%s\"", entry->file, entry->line, entry->function ? entry->function : "?"); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "}"); + if (n > 0) + written += n; + + return written; +} + +static int telemetry_log_entry_format_xml(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter) +{ + int written = 0; + int n; + + if (entry == NULL || buffer == NULL || size == 0) + { + return -1; + } + + n = snprintf(buffer + written, size - written, ""); + if (n > 0) + written += n; + + if (formatter->include_timestamp) + { + n = snprintf(buffer + written, size - written, "%s", entry->timestamp); + if (n > 0) + written += n; + } + + if (formatter->include_level) + { + n = snprintf(buffer + written, size - written, "%s", telemetry_log_level_to_string(entry->level)); + if (n > 0) + written += n; + } + + if (formatter->include_category) + { + n = snprintf(buffer + written, size - written, "%s", entry->category); + if (n > 0) + written += n; + } + + if (formatter->include_thread_info) + { + n = snprintf(buffer + written, size - written, "%u%llu", entry->process_id, (unsigned long long)entry->thread_id); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "%s", entry->message); + if (n > 0) + written += n; + + if (formatter->include_location && entry->file != NULL) + { + n = snprintf(buffer + written, size - written, "%s%d%s", entry->file, entry->line, entry->function ? entry->function : "?"); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, ""); + if (n > 0) + written += n; + + return written; +} + +static int telemetry_log_entry_format_colored(telemetry_log_entry entry, char *buffer, size_t size, telemetry_log_formatter formatter) +{ + int written = 0; + int n; + const char *color_code = telemetry_log_get_color_code(entry->level); + const char *reset_code = telemetry_log_get_reset_code(); + + if (entry == NULL || buffer == NULL || size == 0) + { + return -1; + } + + buffer[0] = '\0'; + + /* Start with color */ + n = snprintf(buffer + written, size - written, "%s", color_code); + if (n > 0) + written += n; + + /* Timestamp */ + if (formatter->include_timestamp) + { + n = snprintf(buffer + written, size - written, "[%s] ", entry->timestamp); + if (n > 0) + written += n; + } + + /* Log level */ + if (formatter->include_level) + { + n = snprintf(buffer + written, size - written, "[%s] ", telemetry_log_level_to_string(entry->level)); + if (n > 0) + written += n; + } + + /* Reset color after level */ + n = snprintf(buffer + written, size - written, "%s", reset_code); + if (n > 0) + written += n; + + /* Category */ + if (formatter->include_category && entry->category[0] != '\0') + { + n = snprintf(buffer + written, size - written, "[%s] ", entry->category); + if (n > 0) + written += n; + } + + /* Thread info */ + if (formatter->include_thread_info) + { + n = snprintf(buffer + written, size - written, "[pid:%u tid:%llu] ", entry->process_id, (unsigned long long)entry->thread_id); + if (n > 0) + written += n; + } + + /* Message */ + n = snprintf(buffer + written, size - written, "%s", entry->message); + if (n > 0) + written += n; + + /* Location */ + if (formatter->include_location && entry->file != NULL) + { + n = snprintf(buffer + written, size - written, " (%s:%d in %s)", entry->file, entry->line, entry->function ? entry->function : "?"); + if (n > 0) + written += n; + } + + return written; +} + +static int telemetry_log_handler_process(telemetry_log_handler handler, telemetry_log_entry entry) +{ + char formatted_buffer[TELEMETRY_LOG_MAX_MESSAGE_SIZE * 2]; + int formatted_length; + + if (handler == NULL || entry == NULL) + { + return 1; + } + + /* Check file rotation if needed */ + if (handler->type == TELEMETRY_LOG_HANDLER_FILE && handler->rotation != TELEMETRY_LOG_ROTATION_NONE) + { + if (telemetry_log_should_rotate(handler)) + { + telemetry_log_rotate_file(handler); + } + } + + /* Use custom callback if provided */ + if (handler->callback != NULL) + { + return handler->callback(entry, handler->user_data); + } + + /* Format the log entry */ + if (handler->formatter != NULL) + { + if (handler->formatter->callback != NULL) + { + formatted_length = handler->formatter->callback(entry, formatted_buffer, sizeof(formatted_buffer), handler->formatter->user_data); + } + else + { + switch (handler->formatter->format) + { + case TELEMETRY_LOG_FORMAT_JSON: + formatted_length = telemetry_log_entry_format_json(entry, formatted_buffer, sizeof(formatted_buffer), handler->formatter); + break; + case TELEMETRY_LOG_FORMAT_XML: + formatted_length = telemetry_log_entry_format_xml(entry, formatted_buffer, sizeof(formatted_buffer), handler->formatter); + break; + case TELEMETRY_LOG_FORMAT_COLORED: + formatted_length = telemetry_log_entry_format_colored(entry, formatted_buffer, sizeof(formatted_buffer), handler->formatter); + break; + case TELEMETRY_LOG_FORMAT_TEXT: + default: + formatted_length = telemetry_log_entry_format_text(entry, formatted_buffer, sizeof(formatted_buffer), handler->formatter); + break; + } + } + } + else + { + /* Default text format */ + telemetry_log_formatter default_formatter_struct; + default_formatter_struct.format = TELEMETRY_LOG_FORMAT_TEXT; + default_formatter_struct.include_timestamp = 1; + default_formatter_struct.include_level = 1; + default_formatter_struct.include_category = 1; + default_formatter_struct.include_location = 1; + default_formatter_struct.include_thread_info = 0; + + formatted_length = telemetry_log_entry_format_text(entry, formatted_buffer, sizeof(formatted_buffer), &default_formatter_struct); + } + + if (formatted_length < 0) + { + return 1; + } + + /* Output to the appropriate destination */ + switch (handler->type) + { + case TELEMETRY_LOG_HANDLER_CONSOLE: + if (entry->level >= TELEMETRY_LOG_LEVEL_ERROR) + { + fprintf(stderr, "%s\n", formatted_buffer); + } + else + { + fprintf(stdout, "%s\n", formatted_buffer); + } + break; + + case TELEMETRY_LOG_HANDLER_FILE: + if (handler->file_handle != NULL) + { + fprintf(handler->file_handle, "%s\n", formatted_buffer); + fflush(handler->file_handle); + } + break; + + case TELEMETRY_LOG_HANDLER_SYSLOG: + /* TODO: Implement syslog handler */ + break; + + case TELEMETRY_LOG_HANDLER_NETWORK: + /* TODO: Implement network handler */ + break; + + case TELEMETRY_LOG_HANDLER_CUSTOM: + /* Custom handlers must provide a callback */ + break; + + default: + return 1; + } + + return 0; +} + +static int telemetry_log_filter_match(telemetry_log_filter filter, telemetry_log_entry entry) +{ + if (filter == NULL || entry == NULL) + { + return 0; + } + + /* Check level */ + if (entry->level < filter->min_level) + { + return 0; + } + + /* Check category pattern if set */ + if (filter->category_pattern[0] != '\0') + { + /* Simple wildcard matching (simplified) */ + if (strcmp(filter->category_pattern, "*") != 0) + { + if (strstr(entry->category, filter->category_pattern) == NULL) + { + return 0; + } + } + } + + /* Use custom callback if provided */ + if (filter->callback != NULL) + { + return filter->callback(entry, filter->user_data); + } + + return 1; +} + +static void telemetry_log_get_timestamp(char *buffer, size_t size) +{ + time_t now; + struct tm *tm_info; + + if (buffer == NULL || size == 0) + { + return; + } + + time(&now); + tm_info = localtime(&now); + + strftime(buffer, size, "%Y-%m-%d %H:%M:%S", tm_info); +} + +static uint64_t telemetry_log_get_thread_id(void) +{ +#if defined(_WIN32) || defined(_WIN64) + return (uint64_t)GetCurrentThreadId(); +#else + return (uint64_t)pthread_self(); +#endif +} + +static const char *telemetry_log_get_color_code(telemetry_log_level_id level) +{ + switch (level) + { + case TELEMETRY_LOG_LEVEL_TRACE: + return "\033[0;37m"; /* White */ + case TELEMETRY_LOG_LEVEL_DEBUG: + return "\033[0;36m"; /* Cyan */ + case TELEMETRY_LOG_LEVEL_INFO: + return "\033[0;32m"; /* Green */ + case TELEMETRY_LOG_LEVEL_WARNING: + return "\033[0;33m"; /* Yellow */ + case TELEMETRY_LOG_LEVEL_ERROR: + return "\033[0;31m"; /* Red */ + case TELEMETRY_LOG_LEVEL_CRITICAL: + return "\033[1;31m"; /* Bold Red */ + default: + return "\033[0m"; /* Reset */ + } +} + +static const char *telemetry_log_get_reset_code(void) +{ + return "\033[0m"; +} + +static int telemetry_log_rotate_file(telemetry_log_handler handler) +{ + char rotated_path[512]; + time_t now; + struct tm *tm_info; + + if (handler == NULL || handler->file_path == NULL) + { + return 1; + } + + /* Close current file */ + if (handler->file_handle != NULL) + { + fclose(handler->file_handle); + handler->file_handle = NULL; + } + + /* Generate rotated filename with timestamp */ + time(&now); + tm_info = localtime(&now); + snprintf(rotated_path, sizeof(rotated_path), "%s.%04d%02d%02d_%02d%02d%02d", + handler->file_path, + tm_info->tm_year + 1900, + tm_info->tm_mon + 1, + tm_info->tm_mday, + tm_info->tm_hour, + tm_info->tm_min, + tm_info->tm_sec); + + /* Rename current file */ + rename(handler->file_path, rotated_path); + + /* Open new file */ + handler->file_handle = fopen(handler->file_path, "a"); + if (handler->file_handle == NULL) + { + return 1; + } + + return 0; +} + +static int telemetry_log_should_rotate(telemetry_log_handler handler) +{ + long file_size; + + if (handler == NULL || handler->file_handle == NULL) + { + return 0; + } + + /* Check size-based rotation */ + if (handler->rotation == TELEMETRY_LOG_ROTATION_SIZE && handler->max_file_size > 0) + { + fseek(handler->file_handle, 0, SEEK_END); + file_size = ftell(handler->file_handle); + + if ((size_t)file_size >= handler->max_file_size) + { + return 1; + } + } + + /* TODO: Implement time-based rotation checks */ + + return 0; +} diff --git a/source/telemetry/source/telemetry_metrics.c b/source/telemetry/source/telemetry_metrics.c new file mode 100644 index 000000000..d73ef7d2f --- /dev/null +++ b/source/telemetry/source/telemetry_metrics.c @@ -0,0 +1,1278 @@ +/* + * Telemetry Library by MetaCall Inc. + * A library for logging and telemetry collection in MetaCall runtime. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +/* -- Headers -- */ + +#include + +#include +#include +#include +#include +#include +#include + +/* -- Private Data -- */ + +static struct telemetry_metric_registry_type global_registry = { + .metric_count = 0, + .exporter_count = 0, + .mutex = NULL +}; + +static int telemetry_metrics_initialized = 0; + +/* -- Private Forward Declarations -- */ + +static int telemetry_metric_export_prometheus(telemetry_metric metric, char *buffer, size_t size); +static int telemetry_metric_export_json(telemetry_metric metric, char *buffer, size_t size); +static int telemetry_metric_export_statsd(telemetry_metric metric, char *buffer, size_t size); +static int telemetry_metric_export_influxdb(telemetry_metric metric, char *buffer, size_t size); +static int telemetry_metric_export_graphite(telemetry_metric metric, char *buffer, size_t size); +static void telemetry_metric_escape_label_value(const char *input, char *output, size_t output_size); +static int telemetry_metric_labels_to_string(telemetry_metric metric, char *buffer, size_t size, const char *format); + +/* -- Methods -- */ + +int telemetry_metrics_initialize(void) +{ + if (telemetry_metrics_initialized) + { + return 0; + } + + /* Initialize the global registry */ + memset(&global_registry, 0, sizeof(struct telemetry_metric_registry_type)); + + /* TODO: Initialize mutex for thread safety */ + + telemetry_metrics_initialized = 1; + + return 0; +} + +int telemetry_metrics_shutdown(void) +{ + size_t i; + + if (!telemetry_metrics_initialized) + { + return 0; + } + + /* Destroy all metrics */ + for (i = 0; i < global_registry.metric_count; ++i) + { + if (global_registry.metrics[i] != NULL) + { + telemetry_metric_destroy(global_registry.metrics[i]); + global_registry.metrics[i] = NULL; + } + } + + /* Destroy all exporters */ + for (i = 0; i < global_registry.exporter_count; ++i) + { + if (global_registry.exporters[i] != NULL) + { + telemetry_metric_exporter_destroy(global_registry.exporters[i]); + global_registry.exporters[i] = NULL; + } + } + + /* TODO: Destroy mutex */ + + telemetry_metrics_initialized = 0; + + return 0; +} + +telemetry_metric_registry telemetry_metrics_get_registry(void) +{ + return &global_registry; +} + +telemetry_metric telemetry_metric_counter_create(const char *name, const char *description, const char *unit) +{ + telemetry_metric metric; + + if (name == NULL) + { + return NULL; + } + + metric = (telemetry_metric)malloc(sizeof(struct telemetry_metric_type)); + if (metric == NULL) + { + return NULL; + } + + memset(metric, 0, sizeof(struct telemetry_metric_type)); + + strncpy(metric->name, name, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->name[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + + if (description != NULL) + { + strncpy(metric->description, description, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->description[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + } + + if (unit != NULL) + { + strncpy(metric->unit, unit, sizeof(metric->unit) - 1); + metric->unit[sizeof(metric->unit) - 1] = '\0'; + } + + metric->type = TELEMETRY_METRIC_TYPE_COUNTER; + metric->created_at = time(NULL); + metric->enabled = 1; + metric->label_count = 0; + + metric->data.counter.value = 0; + metric->data.counter.last_updated = time(NULL); + metric->data.counter.increment_count = 0; + + return metric; +} + +telemetry_metric telemetry_metric_gauge_create(const char *name, const char *description, const char *unit) +{ + telemetry_metric metric; + + if (name == NULL) + { + return NULL; + } + + metric = (telemetry_metric)malloc(sizeof(struct telemetry_metric_type)); + if (metric == NULL) + { + return NULL; + } + + memset(metric, 0, sizeof(struct telemetry_metric_type)); + + strncpy(metric->name, name, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->name[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + + if (description != NULL) + { + strncpy(metric->description, description, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->description[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + } + + if (unit != NULL) + { + strncpy(metric->unit, unit, sizeof(metric->unit) - 1); + metric->unit[sizeof(metric->unit) - 1] = '\0'; + } + + metric->type = TELEMETRY_METRIC_TYPE_GAUGE; + metric->created_at = time(NULL); + metric->enabled = 1; + metric->label_count = 0; + + metric->data.gauge.value = 0.0; + metric->data.gauge.min_value = DBL_MAX; + metric->data.gauge.max_value = -DBL_MAX; + metric->data.gauge.last_updated = time(NULL); + + return metric; +} + +telemetry_metric telemetry_metric_histogram_create(const char *name, const char *description, const char *unit, const double *buckets, size_t bucket_count) +{ + telemetry_metric metric; + size_t i; + + if (name == NULL || buckets == NULL || bucket_count == 0) + { + return NULL; + } + + if (bucket_count > TELEMETRY_METRICS_HISTOGRAM_BUCKETS) + { + return NULL; + } + + metric = (telemetry_metric)malloc(sizeof(struct telemetry_metric_type)); + if (metric == NULL) + { + return NULL; + } + + memset(metric, 0, sizeof(struct telemetry_metric_type)); + + strncpy(metric->name, name, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->name[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + + if (description != NULL) + { + strncpy(metric->description, description, TELEMETRY_METRICS_MAX_NAME_SIZE - 1); + metric->description[TELEMETRY_METRICS_MAX_NAME_SIZE - 1] = '\0'; + } + + if (unit != NULL) + { + strncpy(metric->unit, unit, sizeof(metric->unit) - 1); + metric->unit[sizeof(metric->unit) - 1] = '\0'; + } + + metric->type = TELEMETRY_METRIC_TYPE_HISTOGRAM; + metric->created_at = time(NULL); + metric->enabled = 1; + metric->label_count = 0; + + metric->data.histogram.bucket_count = bucket_count; + metric->data.histogram.total_count = 0; + metric->data.histogram.sum = 0.0; + metric->data.histogram.min = DBL_MAX; + metric->data.histogram.max = -DBL_MAX; + + for (i = 0; i < bucket_count; ++i) + { + metric->data.histogram.buckets[i].upper_bound = buckets[i]; + metric->data.histogram.buckets[i].count = 0; + } + + return metric; +} + +int telemetry_metric_register(telemetry_metric metric) +{ + if (metric == NULL) + { + return 1; + } + + if (!telemetry_metrics_initialized) + { + telemetry_metrics_initialize(); + } + + if (global_registry.metric_count >= TELEMETRY_METRICS_MAX_METRICS) + { + return 1; + } + + /* Check for duplicate names */ + if (telemetry_metric_find(metric->name) != NULL) + { + return 1; + } + + global_registry.metrics[global_registry.metric_count++] = metric; + + return 0; +} + +int telemetry_metric_unregister(telemetry_metric metric) +{ + size_t i, j; + + if (metric == NULL) + { + return 1; + } + + for (i = 0; i < global_registry.metric_count; ++i) + { + if (global_registry.metrics[i] == metric) + { + /* Shift remaining metrics */ + for (j = i; j < global_registry.metric_count - 1; ++j) + { + global_registry.metrics[j] = global_registry.metrics[j + 1]; + } + + global_registry.metric_count--; + return 0; + } + } + + return 1; +} + +telemetry_metric telemetry_metric_find(const char *name) +{ + size_t i; + + if (name == NULL) + { + return NULL; + } + + for (i = 0; i < global_registry.metric_count; ++i) + { + if (global_registry.metrics[i] != NULL) + { + if (strcmp(global_registry.metrics[i]->name, name) == 0) + { + return global_registry.metrics[i]; + } + } + } + + return NULL; +} + +void telemetry_metric_destroy(telemetry_metric metric) +{ + if (metric == NULL) + { + return; + } + + free(metric); +} + +int telemetry_metric_add_label(telemetry_metric metric, const char *key, const char *value) +{ + if (metric == NULL || key == NULL || value == NULL) + { + return 1; + } + + if (metric->label_count >= TELEMETRY_METRICS_MAX_LABELS) + { + return 1; + } + + strncpy(metric->labels[metric->label_count].key, key, TELEMETRY_METRICS_MAX_LABEL_SIZE - 1); + metric->labels[metric->label_count].key[TELEMETRY_METRICS_MAX_LABEL_SIZE - 1] = '\0'; + + strncpy(metric->labels[metric->label_count].value, value, TELEMETRY_METRICS_MAX_LABEL_SIZE - 1); + metric->labels[metric->label_count].value[TELEMETRY_METRICS_MAX_LABEL_SIZE - 1] = '\0'; + + metric->label_count++; + + return 0; +} + +int telemetry_metric_counter_increment(telemetry_metric metric, uint64_t value) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_COUNTER) + { + return 1; + } + + if (!metric->enabled) + { + return 0; + } + + metric->data.counter.value += value; + metric->data.counter.increment_count++; + metric->data.counter.last_updated = time(NULL); + + return 0; +} + +int telemetry_metric_gauge_set(telemetry_metric metric, double value) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_GAUGE) + { + return 1; + } + + if (!metric->enabled) + { + return 0; + } + + metric->data.gauge.value = value; + metric->data.gauge.last_updated = time(NULL); + + if (value < metric->data.gauge.min_value) + { + metric->data.gauge.min_value = value; + } + + if (value > metric->data.gauge.max_value) + { + metric->data.gauge.max_value = value; + } + + return 0; +} + +int telemetry_metric_gauge_increment(telemetry_metric metric, double value) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_GAUGE) + { + return 1; + } + + return telemetry_metric_gauge_set(metric, metric->data.gauge.value + value); +} + +int telemetry_metric_gauge_decrement(telemetry_metric metric, double value) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_GAUGE) + { + return 1; + } + + return telemetry_metric_gauge_set(metric, metric->data.gauge.value - value); +} + +int telemetry_metric_histogram_observe(telemetry_metric metric, double value) +{ + size_t i; + + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_HISTOGRAM) + { + return 1; + } + + if (!metric->enabled) + { + return 0; + } + + /* Update sum and count */ + metric->data.histogram.sum += value; + metric->data.histogram.total_count++; + + /* Update min and max */ + if (value < metric->data.histogram.min) + { + metric->data.histogram.min = value; + } + + if (value > metric->data.histogram.max) + { + metric->data.histogram.max = value; + } + + /* Update buckets */ + for (i = 0; i < metric->data.histogram.bucket_count; ++i) + { + if (value <= metric->data.histogram.buckets[i].upper_bound) + { + metric->data.histogram.buckets[i].count++; + break; + } + } + + return 0; +} + +uint64_t telemetry_metric_counter_get(telemetry_metric metric) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_COUNTER) + { + return 0; + } + + return metric->data.counter.value; +} + +double telemetry_metric_gauge_get(telemetry_metric metric) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_GAUGE) + { + return 0.0; + } + + return metric->data.gauge.value; +} + +int telemetry_metric_histogram_get_stats(telemetry_metric metric, uint64_t *count, double *sum, double *min, double *max) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_HISTOGRAM) + { + return 1; + } + + if (count != NULL) + { + *count = metric->data.histogram.total_count; + } + + if (sum != NULL) + { + *sum = metric->data.histogram.sum; + } + + if (min != NULL) + { + *min = metric->data.histogram.min; + } + + if (max != NULL) + { + *max = metric->data.histogram.max; + } + + return 0; +} + +telemetry_metric_exporter telemetry_metric_exporter_create(telemetry_metric_export_format_id format, const char *endpoint, int push_interval) +{ + telemetry_metric_exporter exporter; + + exporter = (telemetry_metric_exporter)malloc(sizeof(struct telemetry_metric_exporter_type)); + if (exporter == NULL) + { + return NULL; + } + + memset(exporter, 0, sizeof(struct telemetry_metric_exporter_type)); + + exporter->format = format; + exporter->callback = NULL; + exporter->user_data = NULL; + exporter->push_interval = push_interval; + exporter->last_export = time(NULL); + exporter->enabled = 1; + + if (endpoint != NULL) + { + strncpy(exporter->endpoint, endpoint, sizeof(exporter->endpoint) - 1); + exporter->endpoint[sizeof(exporter->endpoint) - 1] = '\0'; + } + + return exporter; +} + +void telemetry_metric_exporter_destroy(telemetry_metric_exporter exporter) +{ + if (exporter == NULL) + { + return; + } + + free(exporter); +} + +int telemetry_metric_exporter_register(telemetry_metric_exporter exporter) +{ + if (exporter == NULL) + { + return 1; + } + + if (global_registry.exporter_count >= TELEMETRY_METRICS_MAX_EXPORTERS) + { + return 1; + } + + global_registry.exporters[global_registry.exporter_count++] = exporter; + + return 0; +} + +int telemetry_metric_exporter_unregister(telemetry_metric_exporter exporter) +{ + size_t i, j; + + if (exporter == NULL) + { + return 1; + } + + for (i = 0; i < global_registry.exporter_count; ++i) + { + if (global_registry.exporters[i] == exporter) + { + /* Shift remaining exporters */ + for (j = i; j < global_registry.exporter_count - 1; ++j) + { + global_registry.exporters[j] = global_registry.exporters[j + 1]; + } + + global_registry.exporter_count--; + return 0; + } + } + + return 1; +} + +int telemetry_metric_exporter_set_callback(telemetry_metric_exporter exporter, telemetry_metric_exporter_callback callback, void *user_data) +{ + if (exporter == NULL || callback == NULL) + { + return 1; + } + + exporter->callback = callback; + exporter->user_data = user_data; + + return 0; +} + +int telemetry_metrics_export_all(void) +{ + size_t i; + time_t now = time(NULL); + int result = 0; + + /* Process each exporter */ + for (i = 0; i < global_registry.exporter_count; ++i) + { + telemetry_metric_exporter exporter = global_registry.exporters[i]; + + if (exporter == NULL || !exporter->enabled) + { + continue; + } + + /* Check if it's time to export */ + if (exporter->push_interval > 0) + { + if (difftime(now, exporter->last_export) < exporter->push_interval) + { + continue; + } + } + + /* Export metrics */ + char buffer[65536]; + int written = telemetry_metrics_export_to_buffer(exporter->format, buffer, sizeof(buffer)); + + if (written > 0) + { + /* TODO: Send buffer to endpoint (file, network, etc.) */ + if (exporter->endpoint[0] != '\0') + { + FILE *fp = fopen(exporter->endpoint, "a"); + if (fp != NULL) + { + fprintf(fp, "%s", buffer); + fclose(fp); + } + } + + exporter->last_export = now; + } + else + { + result = 1; + } + } + + return result; +} + +int telemetry_metrics_export_to_buffer(telemetry_metric_export_format_id format, char *buffer, size_t size) +{ + size_t i; + int total_written = 0; + char metric_buffer[4096]; + + if (buffer == NULL || size == 0) + { + return -1; + } + + buffer[0] = '\0'; + + /* Export each metric */ + for (i = 0; i < global_registry.metric_count; ++i) + { + telemetry_metric metric = global_registry.metrics[i]; + + if (metric == NULL || !metric->enabled) + { + continue; + } + + int written = telemetry_metric_export(metric, format, metric_buffer, sizeof(metric_buffer)); + + if (written > 0 && (size_t)(total_written + written) < size) + { + strcat(buffer, metric_buffer); + total_written += written; + } + } + + return total_written; +} + +int telemetry_metric_export(telemetry_metric metric, telemetry_metric_export_format_id format, char *buffer, size_t size) +{ + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + switch (format) + { + case TELEMETRY_METRIC_EXPORT_PROMETHEUS: + return telemetry_metric_export_prometheus(metric, buffer, size); + + case TELEMETRY_METRIC_EXPORT_JSON: + return telemetry_metric_export_json(metric, buffer, size); + + case TELEMETRY_METRIC_EXPORT_STATSD: + return telemetry_metric_export_statsd(metric, buffer, size); + + case TELEMETRY_METRIC_EXPORT_INFLUXDB: + return telemetry_metric_export_influxdb(metric, buffer, size); + + case TELEMETRY_METRIC_EXPORT_GRAPHITE: + return telemetry_metric_export_graphite(metric, buffer, size); + + default: + return -1; + } +} + +int telemetry_metric_counter_reset(telemetry_metric metric) +{ + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_COUNTER) + { + return 1; + } + + metric->data.counter.value = 0; + metric->data.counter.increment_count = 0; + metric->data.counter.last_updated = time(NULL); + + return 0; +} + +int telemetry_metric_histogram_reset(telemetry_metric metric) +{ + size_t i; + + if (metric == NULL || metric->type != TELEMETRY_METRIC_TYPE_HISTOGRAM) + { + return 1; + } + + metric->data.histogram.total_count = 0; + metric->data.histogram.sum = 0.0; + metric->data.histogram.min = DBL_MAX; + metric->data.histogram.max = -DBL_MAX; + + for (i = 0; i < metric->data.histogram.bucket_count; ++i) + { + metric->data.histogram.buckets[i].count = 0; + } + + return 0; +} + +int telemetry_metrics_reset_all(void) +{ + size_t i; + + for (i = 0; i < global_registry.metric_count; ++i) + { + telemetry_metric metric = global_registry.metrics[i]; + + if (metric == NULL) + { + continue; + } + + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + telemetry_metric_counter_reset(metric); + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + telemetry_metric_histogram_reset(metric); + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + /* Gauges typically aren't reset */ + break; + + default: + break; + } + } + + return 0; +} + +size_t telemetry_metrics_get_count(void) +{ + return global_registry.metric_count; +} + +double telemetry_metric_aggregate(telemetry_metric metric, telemetry_metric_aggregation_id aggregation) +{ + if (metric == NULL) + { + return 0.0; + } + + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + return (double)metric->data.counter.value; + + case TELEMETRY_METRIC_TYPE_GAUGE: + switch (aggregation) + { + case TELEMETRY_METRIC_AGGREGATION_SUM: + case TELEMETRY_METRIC_AGGREGATION_AVG: + return metric->data.gauge.value; + + case TELEMETRY_METRIC_AGGREGATION_MIN: + return metric->data.gauge.min_value; + + case TELEMETRY_METRIC_AGGREGATION_MAX: + return metric->data.gauge.max_value; + + default: + return metric->data.gauge.value; + } + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + switch (aggregation) + { + case TELEMETRY_METRIC_AGGREGATION_SUM: + return metric->data.histogram.sum; + + case TELEMETRY_METRIC_AGGREGATION_AVG: + if (metric->data.histogram.total_count > 0) + { + return metric->data.histogram.sum / metric->data.histogram.total_count; + } + return 0.0; + + case TELEMETRY_METRIC_AGGREGATION_MIN: + return metric->data.histogram.min; + + case TELEMETRY_METRIC_AGGREGATION_MAX: + return metric->data.histogram.max; + + case TELEMETRY_METRIC_AGGREGATION_COUNT: + return (double)metric->data.histogram.total_count; + + default: + return 0.0; + } + + default: + return 0.0; + } +} + +/* -- Private Methods -- */ + +static int telemetry_metric_export_prometheus(telemetry_metric metric, char *buffer, size_t size) +{ + int written = 0; + int n; + char labels_buf[512]; + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + buffer[0] = '\0'; + + /* Format labels */ + telemetry_metric_labels_to_string(metric, labels_buf, sizeof(labels_buf), "prometheus"); + + /* Add HELP and TYPE lines */ + if (metric->description[0] != '\0') + { + n = snprintf(buffer + written, size - written, "# HELP %s %s\n", metric->name, metric->description); + if (n > 0) + written += n; + } + + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + n = snprintf(buffer + written, size - written, "# TYPE %s counter\n", metric->name); + if (n > 0) + written += n; + n = snprintf(buffer + written, size - written, "%s%s %llu\n", metric->name, labels_buf, (unsigned long long)metric->data.counter.value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + n = snprintf(buffer + written, size - written, "# TYPE %s gauge\n", metric->name); + if (n > 0) + written += n; + n = snprintf(buffer + written, size - written, "%s%s %.6f\n", metric->name, labels_buf, metric->data.gauge.value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + { + size_t i; + n = snprintf(buffer + written, size - written, "# TYPE %s histogram\n", metric->name); + if (n > 0) + written += n; + + for (i = 0; i < metric->data.histogram.bucket_count; ++i) + { + n = snprintf(buffer + written, size - written, "%s_bucket{le=\"%.2f\"%s} %llu\n", + metric->name, + metric->data.histogram.buckets[i].upper_bound, + labels_buf + 1, /* Skip opening brace */ + (unsigned long long)metric->data.histogram.buckets[i].count); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "%s_sum%s %.6f\n", metric->name, labels_buf, metric->data.histogram.sum); + if (n > 0) + written += n; + n = snprintf(buffer + written, size - written, "%s_count%s %llu\n", metric->name, labels_buf, (unsigned long long)metric->data.histogram.total_count); + if (n > 0) + written += n; + break; + } + + default: + break; + } + + return written; +} + +static int telemetry_metric_export_json(telemetry_metric metric, char *buffer, size_t size) +{ + int written = 0; + int n; + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + n = snprintf(buffer + written, size - written, "{\"name\":\"%s\",\"type\":\"%s\"", + metric->name, + metric->type == TELEMETRY_METRIC_TYPE_COUNTER ? "counter" : (metric->type == TELEMETRY_METRIC_TYPE_GAUGE ? "gauge" : "histogram")); + if (n > 0) + written += n; + + if (metric->description[0] != '\0') + { + n = snprintf(buffer + written, size - written, ",\"description\":\"%s\"", metric->description); + if (n > 0) + written += n; + } + + if (metric->unit[0] != '\0') + { + n = snprintf(buffer + written, size - written, ",\"unit\":\"%s\"", metric->unit); + if (n > 0) + written += n; + } + + /* Add value */ + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + n = snprintf(buffer + written, size - written, ",\"value\":%llu", (unsigned long long)metric->data.counter.value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + n = snprintf(buffer + written, size - written, ",\"value\":%.6f,\"min\":%.6f,\"max\":%.6f", + metric->data.gauge.value, + metric->data.gauge.min_value, + metric->data.gauge.max_value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + n = snprintf(buffer + written, size - written, ",\"count\":%llu,\"sum\":%.6f,\"min\":%.6f,\"max\":%.6f", + (unsigned long long)metric->data.histogram.total_count, + metric->data.histogram.sum, + metric->data.histogram.min, + metric->data.histogram.max); + if (n > 0) + written += n; + break; + + default: + break; + } + + /* Add labels if present */ + if (metric->label_count > 0) + { + size_t i; + n = snprintf(buffer + written, size - written, ",\"labels\":{"); + if (n > 0) + written += n; + + for (i = 0; i < metric->label_count; ++i) + { + n = snprintf(buffer + written, size - written, "%s\"%s\":\"%s\"", + i > 0 ? "," : "", + metric->labels[i].key, + metric->labels[i].value); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "}"); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "}\n"); + if (n > 0) + written += n; + + return written; +} + +static int telemetry_metric_export_statsd(telemetry_metric metric, char *buffer, size_t size) +{ + int written = 0; + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + written = snprintf(buffer, size, "%s:%llu|c\n", metric->name, (unsigned long long)metric->data.counter.value); + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + written = snprintf(buffer, size, "%s:%.6f|g\n", metric->name, metric->data.gauge.value); + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + written = snprintf(buffer, size, "%s:%.6f|h\n", metric->name, metric->data.histogram.sum); + break; + + default: + written = -1; + break; + } + + return written; +} + +static int telemetry_metric_export_influxdb(telemetry_metric metric, char *buffer, size_t size) +{ + int written = 0; + int n; + char labels_buf[512]; + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + /* Format labels as tags */ + telemetry_metric_labels_to_string(metric, labels_buf, sizeof(labels_buf), "influxdb"); + + /* Measurement name */ + n = snprintf(buffer + written, size - written, "%s%s ", metric->name, labels_buf); + if (n > 0) + written += n; + + /* Fields */ + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + n = snprintf(buffer + written, size - written, "value=%llui", (unsigned long long)metric->data.counter.value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + n = snprintf(buffer + written, size - written, "value=%.6f,min=%.6f,max=%.6f", + metric->data.gauge.value, + metric->data.gauge.min_value, + metric->data.gauge.max_value); + if (n > 0) + written += n; + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + n = snprintf(buffer + written, size - written, "count=%llui,sum=%.6f,min=%.6f,max=%.6f", + (unsigned long long)metric->data.histogram.total_count, + metric->data.histogram.sum, + metric->data.histogram.min, + metric->data.histogram.max); + if (n > 0) + written += n; + break; + + default: + break; + } + + n = snprintf(buffer + written, size - written, "\n"); + if (n > 0) + written += n; + + return written; +} + +static int telemetry_metric_export_graphite(telemetry_metric metric, char *buffer, size_t size) +{ + int written = 0; + time_t now = time(NULL); + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + switch (metric->type) + { + case TELEMETRY_METRIC_TYPE_COUNTER: + written = snprintf(buffer, size, "%s %llu %ld\n", + metric->name, + (unsigned long long)metric->data.counter.value, + (long)now); + break; + + case TELEMETRY_METRIC_TYPE_GAUGE: + written = snprintf(buffer, size, "%s %.6f %ld\n", + metric->name, + metric->data.gauge.value, + (long)now); + break; + + case TELEMETRY_METRIC_TYPE_HISTOGRAM: + { + int n; + n = snprintf(buffer + written, size - written, "%s.count %llu %ld\n", + metric->name, + (unsigned long long)metric->data.histogram.total_count, + (long)now); + if (n > 0) + written += n; + + n = snprintf(buffer + written, size - written, "%s.sum %.6f %ld\n", + metric->name, + metric->data.histogram.sum, + (long)now); + if (n > 0) + written += n; + break; + } + + default: + written = -1; + break; + } + + return written; +} + +static void telemetry_metric_escape_label_value(const char *input, char *output, size_t output_size) +{ + size_t i, j; + size_t input_len = strlen(input); + + if (input == NULL || output == NULL || output_size == 0) + { + return; + } + + for (i = 0, j = 0; i < input_len && j < output_size - 1; ++i) + { + char c = input[i]; + + /* Escape special characters */ + if (c == '"' || c == '\\' || c == '\n') + { + if (j < output_size - 2) + { + output[j++] = '\\'; + output[j++] = c == '\n' ? 'n' : c; + } + } + else + { + output[j++] = c; + } + } + + output[j] = '\0'; +} + +static int telemetry_metric_labels_to_string(telemetry_metric metric, char *buffer, size_t size, const char *format) +{ + size_t i; + int written = 0; + int n; + char escaped[TELEMETRY_METRICS_MAX_LABEL_SIZE * 2]; + + if (metric == NULL || buffer == NULL || size == 0) + { + return -1; + } + + buffer[0] = '\0'; + + if (metric->label_count == 0) + { + return 0; + } + + if (strcmp(format, "prometheus") == 0) + { + n = snprintf(buffer + written, size - written, "{"); + if (n > 0) + written += n; + + for (i = 0; i < metric->label_count; ++i) + { + telemetry_metric_escape_label_value(metric->labels[i].value, escaped, sizeof(escaped)); + + n = snprintf(buffer + written, size - written, "%s%s=\"%s\"", + i > 0 ? "," : "", + metric->labels[i].key, + escaped); + if (n > 0) + written += n; + } + + n = snprintf(buffer + written, size - written, "}"); + if (n > 0) + written += n; + } + else if (strcmp(format, "influxdb") == 0) + { + for (i = 0; i < metric->label_count; ++i) + { + n = snprintf(buffer + written, size - written, ",%s=%s", + metric->labels[i].key, + metric->labels[i].value); + if (n > 0) + written += n; + } + } + + return written; +} diff --git a/source/telemetry/test/test_log.c b/source/telemetry/test/test_log.c new file mode 100644 index 000000000..46e492cf2 --- /dev/null +++ b/source/telemetry/test/test_log.c @@ -0,0 +1,314 @@ +/* + * Telemetry Library by MetaCall Inc. + * Test suite for telemetry logging system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include + +#include +#include +#include + +/* Test initialization and shutdown */ +void test_log_init_shutdown(void) +{ + printf("Testing log initialization and shutdown...\n"); + + int result = telemetry_log_initialize(); + assert(result == 0); + + result = telemetry_log_shutdown(); + assert(result == 0); + + printf("✓ Log initialization and shutdown test passed\n"); +} + +/* Test log level management */ +void test_log_levels(void) +{ + printf("Testing log levels...\n"); + + telemetry_log_initialize(); + + telemetry_log_set_level(TELEMETRY_LOG_LEVEL_DEBUG); + assert(telemetry_log_get_level() == TELEMETRY_LOG_LEVEL_DEBUG); + + telemetry_log_set_level(TELEMETRY_LOG_LEVEL_ERROR); + assert(telemetry_log_get_level() == TELEMETRY_LOG_LEVEL_ERROR); + + telemetry_log_shutdown(); + + printf("✓ Log levels test passed\n"); +} + +/* Test log level string conversion */ +void test_log_level_strings(void) +{ + printf("Testing log level string conversion...\n"); + + telemetry_log_initialize(); + + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_TRACE), "TRACE") == 0); + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_DEBUG), "DEBUG") == 0); + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_INFO), "INFO") == 0); + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_WARNING), "WARNING") == 0); + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_ERROR), "ERROR") == 0); + assert(strcmp(telemetry_log_level_to_string(TELEMETRY_LOG_LEVEL_CRITICAL), "CRITICAL") == 0); + + assert(telemetry_log_level_from_string("TRACE") == TELEMETRY_LOG_LEVEL_TRACE); + assert(telemetry_log_level_from_string("DEBUG") == TELEMETRY_LOG_LEVEL_DEBUG); + assert(telemetry_log_level_from_string("INFO") == TELEMETRY_LOG_LEVEL_INFO); + assert(telemetry_log_level_from_string("WARNING") == TELEMETRY_LOG_LEVEL_WARNING); + assert(telemetry_log_level_from_string("ERROR") == TELEMETRY_LOG_LEVEL_ERROR); + assert(telemetry_log_level_from_string("CRITICAL") == TELEMETRY_LOG_LEVEL_CRITICAL); + + telemetry_log_shutdown(); + + printf("✓ Log level string conversion test passed\n"); +} + +/* Test handler creation and destruction */ +void test_log_handlers(void) +{ + printf("Testing log handlers...\n"); + + telemetry_log_initialize(); + + telemetry_log_handler console_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_INFO); + assert(console_handler != NULL); + + int result = telemetry_log_handler_register(console_handler); + assert(result == 0); + + result = telemetry_log_handler_unregister(console_handler); + assert(result == 0); + + telemetry_log_handler_destroy(console_handler); + + telemetry_log_shutdown(); + + printf("✓ Log handlers test passed\n"); +} + +/* Test file handler configuration */ +void test_file_handler(void) +{ + printf("Testing file handler...\n"); + + telemetry_log_initialize(); + + telemetry_log_handler file_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_FILE, + TELEMETRY_LOG_LEVEL_DEBUG); + assert(file_handler != NULL); + + int result = telemetry_log_handler_configure_file( + file_handler, + "/tmp/test_telemetry.log", + TELEMETRY_LOG_ROTATION_SIZE, + 1024 * 1024); /* 1MB */ + assert(result == 0); + + result = telemetry_log_handler_register(file_handler); + assert(result == 0); + + telemetry_log_handler_unregister(file_handler); + telemetry_log_handler_destroy(file_handler); + + telemetry_log_shutdown(); + + printf("✓ File handler test passed\n"); +} + +/* Test formatter creation */ +void test_log_formatters(void) +{ + printf("Testing log formatters...\n"); + + telemetry_log_initialize(); + + telemetry_log_formatter text_formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_TEXT); + assert(text_formatter != NULL); + + int result = telemetry_log_formatter_configure(text_formatter, 1, 1, 1, 1, 0); + assert(result == 0); + + telemetry_log_formatter_destroy(text_formatter); + + telemetry_log_formatter json_formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_JSON); + assert(json_formatter != NULL); + telemetry_log_formatter_destroy(json_formatter); + + telemetry_log_shutdown(); + + printf("✓ Log formatters test passed\n"); +} + +/* Test filter creation */ +void test_log_filters(void) +{ + printf("Testing log filters...\n"); + + telemetry_log_initialize(); + + telemetry_log_filter filter = telemetry_log_filter_create(); + assert(filter != NULL); + + int result = telemetry_log_filter_set_category(filter, "test.*"); + assert(result == 0); + + result = telemetry_log_filter_set_level(filter, TELEMETRY_LOG_LEVEL_WARNING); + assert(result == 0); + + result = telemetry_log_filter_register(filter); + assert(result == 0); + + result = telemetry_log_filter_unregister(filter); + assert(result == 0); + + telemetry_log_filter_destroy(filter); + + telemetry_log_shutdown(); + + printf("✓ Log filters test passed\n"); +} + +/* Test logging messages */ +void test_log_messages(void) +{ + printf("Testing log messages...\n"); + + telemetry_log_initialize(); + telemetry_log_set_level(TELEMETRY_LOG_LEVEL_TRACE); + + telemetry_log_handler console_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_TRACE); + + telemetry_log_formatter formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_TEXT); + telemetry_log_handler_set_formatter(console_handler, formatter); + telemetry_log_handler_register(console_handler); + + TELEMETRY_LOG_TRACE("test", "This is a trace message"); + TELEMETRY_LOG_DEBUG("test", "This is a debug message"); + TELEMETRY_LOG_INFO("test", "This is an info message"); + TELEMETRY_LOG_WARNING("test", "This is a warning message"); + TELEMETRY_LOG_ERROR("test", "This is an error message"); + TELEMETRY_LOG_CRITICAL("test", "This is a critical message"); + + telemetry_log_flush(); + + telemetry_log_handler_unregister(console_handler); + telemetry_log_handler_destroy(console_handler); + + telemetry_log_shutdown(); + + printf("✓ Log messages test passed\n"); +} + +/* Test colored output formatter */ +void test_colored_formatter(void) +{ + printf("Testing colored formatter...\n"); + + telemetry_log_initialize(); + + telemetry_log_handler console_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_TRACE); + + telemetry_log_formatter colored_formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_COLORED); + telemetry_log_handler_set_formatter(console_handler, colored_formatter); + telemetry_log_handler_register(console_handler); + + TELEMETRY_LOG_INFO("test", "Testing colored output"); + TELEMETRY_LOG_ERROR("test", "Testing colored error output"); + + telemetry_log_handler_unregister(console_handler); + telemetry_log_handler_destroy(console_handler); + + telemetry_log_shutdown(); + + printf("✓ Colored formatter test passed\n"); +} + +/* Test JSON formatter */ +void test_json_formatter(void) +{ + printf("Testing JSON formatter...\n"); + + telemetry_log_initialize(); + + telemetry_log_handler console_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_INFO); + + telemetry_log_formatter json_formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_JSON); + telemetry_log_handler_set_formatter(console_handler, json_formatter); + telemetry_log_handler_register(console_handler); + + TELEMETRY_LOG_INFO("test", "Testing JSON output"); + + telemetry_log_handler_unregister(console_handler); + telemetry_log_handler_destroy(console_handler); + + telemetry_log_shutdown(); + + printf("✓ JSON formatter test passed\n"); +} + +/* Test XML formatter */ +void test_xml_formatter(void) +{ + printf("Testing XML formatter...\n"); + + telemetry_log_initialize(); + + telemetry_log_handler console_handler = telemetry_log_handler_create( + TELEMETRY_LOG_HANDLER_CONSOLE, + TELEMETRY_LOG_LEVEL_INFO); + + telemetry_log_formatter xml_formatter = telemetry_log_formatter_create(TELEMETRY_LOG_FORMAT_XML); + telemetry_log_handler_set_formatter(console_handler, xml_formatter); + telemetry_log_handler_register(console_handler); + + TELEMETRY_LOG_INFO("test", "Testing XML output"); + + telemetry_log_handler_unregister(console_handler); + telemetry_log_handler_destroy(console_handler); + + telemetry_log_shutdown(); + + printf("✓ XML formatter test passed\n"); +} + +/* Main test runner */ +int main(void) +{ + printf("=== Telemetry Logging System Tests ===\n\n"); + + test_log_init_shutdown(); + test_log_levels(); + test_log_level_strings(); + test_log_handlers(); + test_file_handler(); + test_log_formatters(); + test_log_filters(); + test_log_messages(); + test_colored_formatter(); + test_json_formatter(); + test_xml_formatter(); + + printf("\n=== All Tests Passed ===\n"); + + return 0; +} diff --git a/source/telemetry/test/test_metrics.c b/source/telemetry/test/test_metrics.c new file mode 100644 index 000000000..065e3d024 --- /dev/null +++ b/source/telemetry/test/test_metrics.c @@ -0,0 +1,460 @@ +/* + * Telemetry Library by MetaCall Inc. + * Test suite for telemetry metrics system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +#include + +#include +#include +#include + +/* Test initialization and shutdown */ +void test_metrics_init_shutdown(void) +{ + printf("Testing metrics initialization and shutdown...\n"); + + int result = telemetry_metrics_initialize(); + assert(result == 0); + + result = telemetry_metrics_shutdown(); + assert(result == 0); + + printf("✓ Metrics initialization and shutdown test passed\n"); +} + +/* Test counter creation and operations */ +void test_counter_metrics(void) +{ + printf("Testing counter metrics...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric counter = telemetry_metric_counter_create( + "test_counter", + "A test counter", + "requests"); + assert(counter != NULL); + + int result = telemetry_metric_register(counter); + assert(result == 0); + + /* Test increment */ + result = telemetry_metric_counter_increment(counter, 5); + assert(result == 0); + assert(telemetry_metric_counter_get(counter) == 5); + + result = telemetry_metric_counter_increment(counter, 3); + assert(result == 0); + assert(telemetry_metric_counter_get(counter) == 8); + + /* Test reset */ + result = telemetry_metric_counter_reset(counter); + assert(result == 0); + assert(telemetry_metric_counter_get(counter) == 0); + + telemetry_metric_unregister(counter); + telemetry_metric_destroy(counter); + + telemetry_metrics_shutdown(); + + printf("✓ Counter metrics test passed\n"); +} + +/* Test gauge creation and operations */ +void test_gauge_metrics(void) +{ + printf("Testing gauge metrics...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric gauge = telemetry_metric_gauge_create( + "test_gauge", + "A test gauge", + "bytes"); + assert(gauge != NULL); + + int result = telemetry_metric_register(gauge); + assert(result == 0); + + /* Test set */ + result = telemetry_metric_gauge_set(gauge, 42.5); + assert(result == 0); + assert(telemetry_metric_gauge_get(gauge) == 42.5); + + /* Test increment */ + result = telemetry_metric_gauge_increment(gauge, 7.5); + assert(result == 0); + assert(telemetry_metric_gauge_get(gauge) == 50.0); + + /* Test decrement */ + result = telemetry_metric_gauge_decrement(gauge, 10.0); + assert(result == 0); + assert(telemetry_metric_gauge_get(gauge) == 40.0); + + telemetry_metric_unregister(gauge); + telemetry_metric_destroy(gauge); + + telemetry_metrics_shutdown(); + + printf("✓ Gauge metrics test passed\n"); +} + +/* Test histogram creation and operations */ +void test_histogram_metrics(void) +{ + printf("Testing histogram metrics...\n"); + + telemetry_metrics_initialize(); + + double buckets[] = { 0.1, 0.5, 1.0, 5.0, 10.0 }; + size_t bucket_count = sizeof(buckets) / sizeof(buckets[0]); + + telemetry_metric histogram = telemetry_metric_histogram_create( + "test_histogram", + "A test histogram", + "seconds", + buckets, + bucket_count); + assert(histogram != NULL); + + int result = telemetry_metric_register(histogram); + assert(result == 0); + + /* Test observations */ + result = telemetry_metric_histogram_observe(histogram, 0.05); + assert(result == 0); + + result = telemetry_metric_histogram_observe(histogram, 0.3); + assert(result == 0); + + result = telemetry_metric_histogram_observe(histogram, 0.8); + assert(result == 0); + + result = telemetry_metric_histogram_observe(histogram, 2.5); + assert(result == 0); + + result = telemetry_metric_histogram_observe(histogram, 7.0); + assert(result == 0); + + /* Test statistics */ + uint64_t count; + double sum, min, max; + result = telemetry_metric_histogram_get_stats(histogram, &count, &sum, &min, &max); + assert(result == 0); + assert(count == 5); + assert(sum == (0.05 + 0.3 + 0.8 + 2.5 + 7.0)); + assert(min == 0.05); + assert(max == 7.0); + + /* Test reset */ + result = telemetry_metric_histogram_reset(histogram); + assert(result == 0); + + result = telemetry_metric_histogram_get_stats(histogram, &count, &sum, &min, &max); + assert(result == 0); + assert(count == 0); + assert(sum == 0.0); + + telemetry_metric_unregister(histogram); + telemetry_metric_destroy(histogram); + + telemetry_metrics_shutdown(); + + printf("✓ Histogram metrics test passed\n"); +} + +/* Test metric labels */ +void test_metric_labels(void) +{ + printf("Testing metric labels...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric counter = telemetry_metric_counter_create( + "http_requests_total", + "Total HTTP requests", + "requests"); + assert(counter != NULL); + + int result = telemetry_metric_add_label(counter, "method", "GET"); + assert(result == 0); + + result = telemetry_metric_add_label(counter, "status", "200"); + assert(result == 0); + + result = telemetry_metric_add_label(counter, "endpoint", "/api/users"); + assert(result == 0); + + result = telemetry_metric_register(counter); + assert(result == 0); + + telemetry_metric_unregister(counter); + telemetry_metric_destroy(counter); + + telemetry_metrics_shutdown(); + + printf("✓ Metric labels test passed\n"); +} + +/* Test metric find */ +void test_metric_find(void) +{ + printf("Testing metric find...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric counter = telemetry_metric_counter_create( + "findable_counter", + "A counter that can be found", + ""); + telemetry_metric_register(counter); + + telemetry_metric found = telemetry_metric_find("findable_counter"); + assert(found != NULL); + assert(found == counter); + + telemetry_metric not_found = telemetry_metric_find("nonexistent_metric"); + assert(not_found == NULL); + + telemetry_metric_unregister(counter); + telemetry_metric_destroy(counter); + + telemetry_metrics_shutdown(); + + printf("✓ Metric find test passed\n"); +} + +/* Test metric exporters */ +void test_metric_exporters(void) +{ + printf("Testing metric exporters...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric_exporter prometheus_exporter = telemetry_metric_exporter_create( + TELEMETRY_METRIC_EXPORT_PROMETHEUS, + "/tmp/metrics.txt", + 60); + assert(prometheus_exporter != NULL); + + int result = telemetry_metric_exporter_register(prometheus_exporter); + assert(result == 0); + + result = telemetry_metric_exporter_unregister(prometheus_exporter); + assert(result == 0); + + telemetry_metric_exporter_destroy(prometheus_exporter); + + telemetry_metrics_shutdown(); + + printf("✓ Metric exporters test passed\n"); +} + +/* Test Prometheus export format */ +void test_prometheus_export(void) +{ + printf("Testing Prometheus export format...\n"); + + telemetry_metrics_initialize(); + + /* Create and register a counter */ + telemetry_metric counter = telemetry_metric_counter_create( + "test_requests_total", + "Total test requests", + "requests"); + telemetry_metric_add_label(counter, "method", "GET"); + telemetry_metric_counter_increment(counter, 42); + telemetry_metric_register(counter); + + /* Create and register a gauge */ + telemetry_metric gauge = telemetry_metric_gauge_create( + "test_memory_usage", + "Test memory usage", + "bytes"); + telemetry_metric_gauge_set(gauge, 1024.0); + telemetry_metric_register(gauge); + + /* Export to Prometheus format */ + char buffer[4096]; + int written = telemetry_metrics_export_to_buffer( + TELEMETRY_METRIC_EXPORT_PROMETHEUS, + buffer, + sizeof(buffer)); + + assert(written > 0); + printf("Prometheus export:\n%s\n", buffer); + + telemetry_metric_unregister(counter); + telemetry_metric_unregister(gauge); + telemetry_metric_destroy(counter); + telemetry_metric_destroy(gauge); + + telemetry_metrics_shutdown(); + + printf("✓ Prometheus export test passed\n"); +} + +/* Test JSON export format */ +void test_json_export(void) +{ + printf("Testing JSON export format...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric counter = telemetry_metric_counter_create( + "api_calls", + "API calls counter", + "calls"); + telemetry_metric_counter_increment(counter, 100); + telemetry_metric_register(counter); + + char buffer[4096]; + int written = telemetry_metrics_export_to_buffer( + TELEMETRY_METRIC_EXPORT_JSON, + buffer, + sizeof(buffer)); + + assert(written > 0); + printf("JSON export:\n%s\n", buffer); + + telemetry_metric_unregister(counter); + telemetry_metric_destroy(counter); + + telemetry_metrics_shutdown(); + + printf("✓ JSON export test passed\n"); +} + +/* Test metric aggregation */ +void test_metric_aggregation(void) +{ + printf("Testing metric aggregation...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric gauge = telemetry_metric_gauge_create( + "temperature", + "Temperature gauge", + "celsius"); + telemetry_metric_register(gauge); + + telemetry_metric_gauge_set(gauge, 20.0); + telemetry_metric_gauge_set(gauge, 25.0); + telemetry_metric_gauge_set(gauge, 15.0); + + double current = telemetry_metric_aggregate(gauge, TELEMETRY_METRIC_AGGREGATION_SUM); + assert(current == 15.0); /* Last value */ + + double min_val = telemetry_metric_aggregate(gauge, TELEMETRY_METRIC_AGGREGATION_MIN); + assert(min_val == 15.0); + + double max_val = telemetry_metric_aggregate(gauge, TELEMETRY_METRIC_AGGREGATION_MAX); + assert(max_val == 25.0); + + telemetry_metric_unregister(gauge); + telemetry_metric_destroy(gauge); + + telemetry_metrics_shutdown(); + + printf("✓ Metric aggregation test passed\n"); +} + +/* Test reset all metrics */ +void test_reset_all_metrics(void) +{ + printf("Testing reset all metrics...\n"); + + telemetry_metrics_initialize(); + + telemetry_metric counter1 = telemetry_metric_counter_create("counter1", "", ""); + telemetry_metric counter2 = telemetry_metric_counter_create("counter2", "", ""); + + telemetry_metric_counter_increment(counter1, 10); + telemetry_metric_counter_increment(counter2, 20); + + telemetry_metric_register(counter1); + telemetry_metric_register(counter2); + + assert(telemetry_metric_counter_get(counter1) == 10); + assert(telemetry_metric_counter_get(counter2) == 20); + + telemetry_metrics_reset_all(); + + assert(telemetry_metric_counter_get(counter1) == 0); + assert(telemetry_metric_counter_get(counter2) == 0); + + telemetry_metric_unregister(counter1); + telemetry_metric_unregister(counter2); + telemetry_metric_destroy(counter1); + telemetry_metric_destroy(counter2); + + telemetry_metrics_shutdown(); + + printf("✓ Reset all metrics test passed\n"); +} + +/* Test metrics count */ +void test_metrics_count(void) +{ + printf("Testing metrics count...\n"); + + telemetry_metrics_initialize(); + + size_t initial_count = telemetry_metrics_get_count(); + + telemetry_metric m1 = telemetry_metric_counter_create("m1", "", ""); + telemetry_metric m2 = telemetry_metric_gauge_create("m2", "", ""); + double buckets[] = { 1.0, 5.0, 10.0 }; + telemetry_metric m3 = telemetry_metric_histogram_create("m3", "", "", buckets, 3); + + telemetry_metric_register(m1); + telemetry_metric_register(m2); + telemetry_metric_register(m3); + + assert(telemetry_metrics_get_count() == initial_count + 3); + + telemetry_metric_unregister(m1); + assert(telemetry_metrics_get_count() == initial_count + 2); + + telemetry_metric_unregister(m2); + telemetry_metric_unregister(m3); + + telemetry_metric_destroy(m1); + telemetry_metric_destroy(m2); + telemetry_metric_destroy(m3); + + telemetry_metrics_shutdown(); + + printf("✓ Metrics count test passed\n"); +} + +/* Main test runner */ +int main(void) +{ + printf("=== Telemetry Metrics System Tests ===\n\n"); + + test_metrics_init_shutdown(); + test_counter_metrics(); + test_gauge_metrics(); + test_histogram_metrics(); + test_metric_labels(); + test_metric_find(); + test_metric_exporters(); + test_prometheus_export(); + test_json_export(); + test_metric_aggregation(); + test_reset_all_metrics(); + test_metrics_count(); + + printf("\n=== All Tests Passed ===\n"); + + return 0; +} diff --git a/source/telemetry/test/test_telemetry.js b/source/telemetry/test/test_telemetry.js new file mode 100644 index 000000000..428cd64a5 --- /dev/null +++ b/source/telemetry/test/test_telemetry.js @@ -0,0 +1,387 @@ +/** + * Telemetry Library JavaScript Tests + * Test suite for JavaScript bindings of the MetaCall telemetry system. + * + * Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre + */ + +'use strict'; + +const telemetry = require('../bindings/javascript/telemetry'); + +const { + Logger, + LogLevel, + LogHandler, + LogFormatter, + HandlerType, + LogFormat, + Counter, + Gauge, + Histogram, + MetricRegistry, + MetricExportFormat, + getLogger +} = telemetry; + +let testsPassed = 0; +let testsFailed = 0; + +function assert(condition, message) { + if (!condition) { + console.error(`✗ Assertion failed: ${message}`); + testsFailed++; + throw new Error(`Assertion failed: ${message}`); + } +} + +function assertEquals(actual, expected, message) { + if (actual !== expected) { + console.error(`✗ ${message}: expected ${expected}, got ${actual}`); + testsFailed++; + throw new Error(`Expected ${expected}, got ${actual}`); + } +} + +function test(name, fn) { + try { + console.log(`Testing ${name}...`); + fn(); + console.log(`✓ ${name} test passed`); + testsPassed++; + } catch (error) { + console.error(`✗ ${name} test failed:`, error.message); + testsFailed++; + } +} + +// Test logger initialization +test('logger initialization', () => { + Logger.initialize(); + Logger.shutdown(); +}); + +// Test log levels +test('log levels', () => { + Logger.initialize(); + + Logger.setLevel(LogLevel.DEBUG); + assertEquals(Logger.getLevel(), LogLevel.DEBUG, "Log level should be DEBUG"); + + Logger.setLevel(LogLevel.ERROR); + assertEquals(Logger.getLevel(), LogLevel.ERROR, "Log level should be ERROR"); + + Logger.shutdown(); +}); + +// Test logging messages +test('logging messages', () => { + Logger.initialize(); + Logger.setLevel(LogLevel.TRACE); + + Logger.trace("test", "This is a trace message"); + Logger.debug("test", "This is a debug message"); + Logger.info("test", "This is an info message"); + Logger.warning("test", "This is a warning message"); + Logger.error("test", "This is an error message"); + Logger.critical("test", "This is a critical message"); + + Logger.shutdown(); +}); + +// Test category logger +test('category logger', () => { + Logger.initialize(); + + const logger = getLogger("myapp"); + logger.info("Starting application"); + logger.debug("Debug information"); + logger.error("An error occurred"); + + Logger.shutdown(); +}); + +// Test log handler +test('log handler', () => { + Logger.initialize(); + + const handler = new LogHandler(HandlerType.CONSOLE, LogLevel.INFO); + Logger.addHandler(handler); + Logger.removeHandler(handler); + + Logger.shutdown(); +}); + +// Test log formatter +test('log formatter', () => { + Logger.initialize(); + + const formatter = new LogFormatter(LogFormat.TEXT); + formatter.configure({ + includeTimestamp: true, + includeLevel: true, + includeCategory: true, + includeLocation: false, + includeThreadInfo: false + }); + + Logger.shutdown(); +}); + +// Test counter metric +test('counter metric', () => { + MetricRegistry.initialize(); + + const counter = new Counter("test_counter", "A test counter", "requests"); + MetricRegistry.register(counter); + + counter.increment(); + assertEquals(counter.get(), 1, "Counter should be 1"); + + counter.increment(5); + assertEquals(counter.get(), 6, "Counter should be 6"); + + counter.reset(); + assertEquals(counter.get(), 0, "Counter should be 0 after reset"); + + MetricRegistry.unregister(counter); + MetricRegistry.shutdown(); +}); + +// Test gauge metric +test('gauge metric', () => { + MetricRegistry.initialize(); + + const gauge = new Gauge("test_gauge", "A test gauge", "bytes"); + MetricRegistry.register(gauge); + + gauge.set(100.0); + assertEquals(gauge.get(), 100.0, "Gauge should be 100.0"); + + gauge.increment(50.0); + assertEquals(gauge.get(), 150.0, "Gauge should be 150.0"); + + gauge.decrement(25.0); + assertEquals(gauge.get(), 125.0, "Gauge should be 125.0"); + + MetricRegistry.unregister(gauge); + MetricRegistry.shutdown(); +}); + +// Test histogram metric +test('histogram metric', () => { + MetricRegistry.initialize(); + + const histogram = new Histogram("test_histogram", "A test histogram", "seconds", + [0.1, 0.5, 1.0, 5.0, 10.0]); + MetricRegistry.register(histogram); + + histogram.observe(0.05); + histogram.observe(0.3); + histogram.observe(0.8); + histogram.observe(2.5); + histogram.observe(7.0); + + const stats = histogram.getStats(); + assertEquals(stats.count, 5, "Histogram count should be 5"); + assertEquals(stats.sum, 10.65, "Histogram sum should be 10.65"); + assertEquals(stats.min, 0.05, "Histogram min should be 0.05"); + assertEquals(stats.max, 7.0, "Histogram max should be 7.0"); + + histogram.reset(); + const resetStats = histogram.getStats(); + assertEquals(resetStats.count, 0, "Histogram count should be 0 after reset"); + + MetricRegistry.unregister(histogram); + MetricRegistry.shutdown(); +}); + +// Test metric labels +test('metric labels', () => { + MetricRegistry.initialize(); + + const counter = new Counter("http_requests_total", "Total HTTP requests", "requests"); + counter.addLabel("method", "GET"); + counter.addLabel("status", "200"); + counter.addLabel("endpoint", "/api/users"); + + counter.increment(42); + MetricRegistry.register(counter); + + const labelsString = counter.getLabelsString(); + assert(labelsString.includes("method"), "Labels should include method"); + assert(labelsString.includes("GET"), "Labels should include GET"); + + MetricRegistry.unregister(counter); + MetricRegistry.shutdown(); +}); + +// Test metric registry +test('metric registry', () => { + MetricRegistry.initialize(); + + const counter = new Counter("registry_counter", "A counter in registry"); + const gauge = new Gauge("registry_gauge", "A gauge in registry"); + + MetricRegistry.register(counter); + MetricRegistry.register(gauge); + + const foundCounter = MetricRegistry.get("registry_counter"); + assert(foundCounter !== null, "Should find counter"); + assertEquals(foundCounter, counter, "Found counter should match"); + + const foundGauge = MetricRegistry.get("registry_gauge"); + assert(foundGauge !== null, "Should find gauge"); + assertEquals(foundGauge, gauge, "Found gauge should match"); + + const notFound = MetricRegistry.get("nonexistent"); + assert(notFound === undefined, "Should not find nonexistent metric"); + + const allMetrics = MetricRegistry.getAll(); + assert(allMetrics.length >= 2, "Should have at least 2 metrics"); + + MetricRegistry.unregister(counter); + MetricRegistry.unregister(gauge); + MetricRegistry.shutdown(); +}); + +// Test Prometheus export +test('Prometheus export', () => { + MetricRegistry.initialize(); + + const counter = new Counter("export_counter", "Counter for export test", "requests"); + counter.increment(100); + MetricRegistry.register(counter); + + const gauge = new Gauge("export_gauge", "Gauge for export test", "bytes"); + gauge.set(1024.0); + MetricRegistry.register(gauge); + + const exportData = MetricRegistry.exportAll(MetricExportFormat.PROMETHEUS); + assert(exportData.includes("export_counter"), "Export should include counter"); + assert(exportData.includes("export_gauge"), "Export should include gauge"); + assert(exportData.includes("counter"), "Export should include counter type"); + assert(exportData.includes("gauge"), "Export should include gauge type"); + + console.log("Prometheus export sample:"); + console.log(exportData.substring(0, 200)); + + MetricRegistry.unregister(counter); + MetricRegistry.unregister(gauge); + MetricRegistry.shutdown(); +}); + +// Test JSON export +test('JSON export', () => { + MetricRegistry.initialize(); + + const counter = new Counter("json_counter", "Counter for JSON test", "requests"); + counter.increment(50); + MetricRegistry.register(counter); + + const exportData = MetricRegistry.exportAll(MetricExportFormat.JSON); + assert(exportData.includes("json_counter"), "Export should include counter name"); + + const parsed = JSON.parse(exportData); + assert(Array.isArray(parsed), "Parsed data should be an array"); + assert(parsed.length > 0, "Should have at least one metric"); + + MetricRegistry.unregister(counter); + MetricRegistry.shutdown(); +}); + +// Test reset all metrics +test('reset all metrics', () => { + MetricRegistry.initialize(); + + const counter1 = new Counter("reset_counter1"); + const counter2 = new Counter("reset_counter2"); + + counter1.increment(10); + counter2.increment(20); + + MetricRegistry.register(counter1); + MetricRegistry.register(counter2); + + assertEquals(counter1.get(), 10, "Counter1 should be 10"); + assertEquals(counter2.get(), 20, "Counter2 should be 20"); + + MetricRegistry.resetAll(); + + assertEquals(counter1.get(), 0, "Counter1 should be 0 after reset"); + assertEquals(counter2.get(), 0, "Counter2 should be 0 after reset"); + + MetricRegistry.unregister(counter1); + MetricRegistry.unregister(counter2); + MetricRegistry.shutdown(); +}); + +// Test metric exporter +test('metric exporter', () => { + MetricRegistry.initialize(); + + const exporter = new telemetry.MetricExporter( + MetricExportFormat.PROMETHEUS, + "/tmp/metrics.txt", + 60 + ); + + exporter.register(); + exporter.unregister(); + + MetricRegistry.shutdown(); +}); + +// Test histogram buckets +test('histogram buckets', () => { + MetricRegistry.initialize(); + + const histogram = new Histogram("bucket_test", "Test buckets", "ms"); + histogram.observe(0.1); + histogram.observe(1.5); + histogram.observe(8.0); + + const buckets = histogram.getBuckets(); + assert(buckets.length > 0, "Should have buckets"); + + MetricRegistry.unregister(histogram); + MetricRegistry.shutdown(); +}); + +// Test gauge min/max tracking +test('gauge min/max tracking', () => { + MetricRegistry.initialize(); + + const gauge = new Gauge("minmax_gauge", "Test min/max"); + MetricRegistry.register(gauge); + + gauge.set(100); + gauge.set(50); + gauge.set(150); + gauge.set(75); + + assertEquals(gauge.getMin(), 50, "Min should be 50"); + assertEquals(gauge.getMax(), 150, "Max should be 150"); + assertEquals(gauge.get(), 75, "Current value should be 75"); + + MetricRegistry.unregister(gauge); + MetricRegistry.shutdown(); +}); + +// Run all tests +console.log("=== JavaScript Telemetry Bindings Tests ===\n"); + +// Allow tests to complete +setTimeout(() => { + console.log(`\n=== Test Results ===`); + console.log(`Tests passed: ${testsPassed}`); + console.log(`Tests failed: ${testsFailed}`); + + if (testsFailed === 0) { + console.log("\n=== All JavaScript Tests Passed ==="); + process.exit(0); + } else { + console.log("\n=== Some Tests Failed ==="); + process.exit(1); + } +}, 100); diff --git a/source/telemetry/test/test_telemetry.py b/source/telemetry/test/test_telemetry.py new file mode 100644 index 000000000..be0770827 --- /dev/null +++ b/source/telemetry/test/test_telemetry.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +""" +Telemetry Library Python Tests +Test suite for Python bindings of the MetaCall telemetry system. + +Copyright (C) 2025 MetaCall Inc., Dhiren Mhatre +""" + +import sys +import os + +# Add the bindings directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../bindings/python')) + +from telemetry import ( + Logger, LogLevel, LogHandler, LogFormatter, HandlerType, LogFormat, + Counter, Gauge, Histogram, MetricRegistry, MetricExportFormat, + get_logger, timed, counted +) + + +def test_logger_initialization(): + """Test logger initialization""" + print("Testing logger initialization...") + Logger.initialize() + Logger.shutdown() + print("✓ Logger initialization test passed") + + +def test_log_levels(): + """Test log level management""" + print("Testing log levels...") + Logger.initialize() + + Logger.set_level(LogLevel.DEBUG) + assert Logger.get_level() == LogLevel.DEBUG + + Logger.set_level(LogLevel.ERROR) + assert Logger.get_level() == LogLevel.ERROR + + Logger.shutdown() + print("✓ Log levels test passed") + + +def test_logging_messages(): + """Test logging various messages""" + print("Testing logging messages...") + Logger.initialize() + Logger.set_level(LogLevel.TRACE) + + Logger.trace("test", "This is a trace message") + Logger.debug("test", "This is a debug message") + Logger.info("test", "This is an info message") + Logger.warning("test", "This is a warning message") + Logger.error("test", "This is an error message") + Logger.critical("test", "This is a critical message") + + Logger.shutdown() + print("✓ Logging messages test passed") + + +def test_category_logger(): + """Test category-scoped logger""" + print("Testing category logger...") + Logger.initialize() + + logger = get_logger("myapp") + logger.info("Starting application") + logger.debug("Debug information") + logger.error("An error occurred") + + Logger.shutdown() + print("✓ Category logger test passed") + + +def test_counter_metric(): + """Test counter metrics""" + print("Testing counter metrics...") + MetricRegistry.initialize() + + counter = Counter("test_counter", "A test counter", "requests") + MetricRegistry.register(counter) + + counter.increment() + assert counter.get() == 1 + + counter.increment(5) + assert counter.get() == 6 + + counter.reset() + assert counter.get() == 0 + + MetricRegistry.unregister(counter) + MetricRegistry.shutdown() + print("✓ Counter metrics test passed") + + +def test_gauge_metric(): + """Test gauge metrics""" + print("Testing gauge metrics...") + MetricRegistry.initialize() + + gauge = Gauge("test_gauge", "A test gauge", "bytes") + MetricRegistry.register(gauge) + + gauge.set(100.0) + assert gauge.get() == 100.0 + + gauge.increment(50.0) + assert gauge.get() == 150.0 + + gauge.decrement(25.0) + assert gauge.get() == 125.0 + + MetricRegistry.unregister(gauge) + MetricRegistry.shutdown() + print("✓ Gauge metrics test passed") + + +def test_histogram_metric(): + """Test histogram metrics""" + print("Testing histogram metrics...") + MetricRegistry.initialize() + + histogram = Histogram("test_histogram", "A test histogram", "seconds", + [0.1, 0.5, 1.0, 5.0, 10.0]) + MetricRegistry.register(histogram) + + histogram.observe(0.05) + histogram.observe(0.3) + histogram.observe(0.8) + histogram.observe(2.5) + histogram.observe(7.0) + + stats = histogram.get_stats() + assert stats['count'] == 5 + assert stats['sum'] == (0.05 + 0.3 + 0.8 + 2.5 + 7.0) + assert stats['min'] == 0.05 + assert stats['max'] == 7.0 + + histogram.reset() + stats = histogram.get_stats() + assert stats['count'] == 0 + + MetricRegistry.unregister(histogram) + MetricRegistry.shutdown() + print("✓ Histogram metrics test passed") + + +def test_metric_labels(): + """Test metric labels""" + print("Testing metric labels...") + MetricRegistry.initialize() + + counter = Counter("http_requests_total", "Total HTTP requests", "requests") + counter.add_label("method", "GET") + counter.add_label("status", "200") + counter.add_label("endpoint", "/api/users") + + counter.increment(42) + MetricRegistry.register(counter) + + MetricRegistry.unregister(counter) + MetricRegistry.shutdown() + print("✓ Metric labels test passed") + + +def test_metric_registry(): + """Test metric registry operations""" + print("Testing metric registry...") + MetricRegistry.initialize() + + counter = Counter("registry_counter", "A counter in registry") + gauge = Gauge("registry_gauge", "A gauge in registry") + + MetricRegistry.register(counter) + MetricRegistry.register(gauge) + + found_counter = MetricRegistry.get("registry_counter") + assert found_counter is not None + assert found_counter == counter + + found_gauge = MetricRegistry.get("registry_gauge") + assert found_gauge is not None + assert found_gauge == gauge + + not_found = MetricRegistry.get("nonexistent") + assert not_found is None + + all_metrics = MetricRegistry.get_all() + assert len(all_metrics) >= 2 + + MetricRegistry.unregister(counter) + MetricRegistry.unregister(gauge) + MetricRegistry.shutdown() + print("✓ Metric registry test passed") + + +def test_prometheus_export(): + """Test Prometheus export format""" + print("Testing Prometheus export...") + MetricRegistry.initialize() + + counter = Counter("export_counter", "Counter for export test", "requests") + counter.increment(100) + MetricRegistry.register(counter) + + gauge = Gauge("export_gauge", "Gauge for export test", "bytes") + gauge.set(1024.0) + MetricRegistry.register(gauge) + + export_data = MetricRegistry.export_all(MetricExportFormat.PROMETHEUS) + assert "export_counter" in export_data + assert "export_gauge" in export_data + assert "counter" in export_data + assert "gauge" in export_data + + print("Prometheus export sample:") + print(export_data[:200]) + + MetricRegistry.unregister(counter) + MetricRegistry.unregister(gauge) + MetricRegistry.shutdown() + print("✓ Prometheus export test passed") + + +def test_reset_all_metrics(): + """Test resetting all metrics""" + print("Testing reset all metrics...") + MetricRegistry.initialize() + + counter1 = Counter("reset_counter1") + counter2 = Counter("reset_counter2") + + counter1.increment(10) + counter2.increment(20) + + MetricRegistry.register(counter1) + MetricRegistry.register(counter2) + + assert counter1.get() == 10 + assert counter2.get() == 20 + + MetricRegistry.reset_all() + + assert counter1.get() == 0 + assert counter2.get() == 0 + + MetricRegistry.unregister(counter1) + MetricRegistry.unregister(counter2) + MetricRegistry.shutdown() + print("✓ Reset all metrics test passed") + + +def test_timed_decorator(): + """Test the timed decorator""" + print("Testing timed decorator...") + MetricRegistry.initialize() + + @timed("function_duration") + def slow_function(): + import time + time.sleep(0.1) + return "done" + + result = slow_function() + assert result == "done" + + histogram = MetricRegistry.get("function_duration") + assert histogram is not None + + MetricRegistry.shutdown() + print("✓ Timed decorator test passed") + + +def test_counted_decorator(): + """Test the counted decorator""" + print("Testing counted decorator...") + MetricRegistry.initialize() + + @counted("function_calls") + def my_function(): + return "called" + + my_function() + my_function() + my_function() + + counter = MetricRegistry.get("function_calls") + assert counter is not None + assert counter.get() == 3 + + MetricRegistry.shutdown() + print("✓ Counted decorator test passed") + + +def run_all_tests(): + """Run all tests""" + print("=== Python Telemetry Bindings Tests ===\n") + + test_logger_initialization() + test_log_levels() + test_logging_messages() + test_category_logger() + test_counter_metric() + test_gauge_metric() + test_histogram_metric() + test_metric_labels() + test_metric_registry() + test_prometheus_export() + test_reset_all_metrics() + test_timed_decorator() + test_counted_decorator() + + print("\n=== All Python Tests Passed ===") + + +if __name__ == "__main__": + run_all_tests()