Skip to content

PurpleSoftSrl/purple_logger_otel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

purple_logger_otel

Pub Version License Dart

OpenTelemetry bridge abstractions for purple_logger — define once, implement for any OTel backend.


Why this package exists

The OpenTelemetry ecosystem has multiple Dart SDKs (dartastic_opentelemetry, open_telemetry, custom vendor implementations, etc.). Without an abstraction layer, every package that wants to bridge purple_logger to OTel must commit to a specific SDK — coupling your logging pipeline to a single OTel backend.

purple_logger_otel solves this with interface segregation. It defines the contracts that every OTel bridge must implement, with zero dependencies on any OpenTelemetry SDK. Your application code depends only on these abstractions. When you need to swap the OTel backend (or evaluate different SDKs), you change one import — nothing else.

This follows the same architectural pattern as Dart's http / cupertino_http / http2_client split:

Package Role OTel SDK dependency
purple_logger_otel Abstract interfaces, severity mapping, trace context None
purple_logger_otel_dartastic Concrete bridge using dartastic_opentelemetry dartastic_opentelemetry
Your custom bridge Concrete bridge for any OTel SDK you choose Whatever you need

Architecture

                       purple_logger
                            │
                            │ (depends on)
                            ▼
                  purple_logger_otel      ← You write code against THIS layer
                   (abstractions)
                   zero OTel deps
                    ╱          ╲
                   ╱            ╲
    purple_logger_otel_dartastic   YourCustomOtelBridge  
    (dartastic_opentelemetry)      (any OTel SDK)

Your application only ever imports purple_logger and purple_logger_otel. The concrete bridge is an implementation detail swapped at the composition root.


Interfaces

OtelLoggerProvider

Abstract provider that extends purple_logger's LoggerProvider. Creates and caches OtelLogger instances by category name.

abstract class OtelLoggerProvider extends LoggerProvider {
  OtelLogger createOtelLogger(String category);   // Subclasses must implement
  @override
  Logger createLogger(String category);            // Caches & delegates
  @override
  void dispose();                                  // Clears cached loggers
}

Responsibilities:

  • Factory for OtelLogger instances (one per category)
  • Logger caching — repeated calls for the same category return the same instance
  • Lifecycle management via dispose()

OtelLogger

Abstract logger that bridges purple_logger log events to any OTel SDK. Implements purple_logger's Logger and the OtelSeverityMapping mixin.

abstract class OtelLogger implements Logger, OtelSeverityMapping {
  final String category;

  OtelLogger({required this.category});

  // Subclasses must implement:
  void emitToOtel({
    required int severityNumber,
    required String severityText,
    required String body,
    required Map<String, Object> attributes,
  });
  TraceContext extractTraceContext();

  // Pre-built: scope merging, error attachment, null filtering, severity mapping
  @override
  void log(PurpleLogLevel level, Object? message, {...});
  @override
  bool isEnabled(PurpleLogLevel level);
  @override
  LoggingScope beginScope(Map<String, Object?> properties);
}

What OtelLogger does for free (no override needed):

  • Merges LoggingScope.currentProperties with caller-supplied properties
  • Attaches error.type, error.message, and error.stackTrace when an error is provided
  • Extracts trace context (traceId, spanId) for log-trace correlation
  • Filters out null values from attributes (OTel cannot ingest nulls)
  • Maps PurpleLogLevel to OTel severity via severityNumber() / severityText()

What you must implement:

  • emitToOtel() — send the assembled log record to your OTel SDK
  • extractTraceContext() — read the current traceId/spanId from your SDK's context

OtelSeverityMapping

Mixin that provides the standard OTel severity mapping for PurpleLogLevel. Mix into any OtelLogger subclass to get severityNumber() and severityText() for free.

mixin OtelSeverityMapping {
  int severityNumber(PurpleLogLevel level);
  String severityText(PurpleLogLevel level);
}

You can also use the standalone functions directly without the mixin:

import 'package:purple_logger/purple_logger.dart';
import 'package:purple_logger_otel/purple_logger_otel.dart';

final num  = severityNumber(PurpleLogLevel.error);  // 17
final text = severityText(PurpleLogLevel.error);     // 'ERROR'

TraceContext

Vendor-neutral value object for trace correlation. Carries the traceId and spanId extracted from the current OTel context.

final class TraceContext {
  final String? traceId;
  final String? spanId;
  bool get isValid => traceId != null && spanId != null;
  const TraceContext({this.traceId, this.spanId});
  static const TraceContext empty = TraceContext();
}

When a span is active (e.g. inside an OTel tracing scope), the concrete bridge populates this with the current trace and span IDs. These are automatically attached to emitted log records, enabling correlation between logs and traces in backends like Jaeger, Tempo, Grafana, or Azure Monitor.

If no span is active, extractTraceContext() returns TraceContext.empty and no trace fields are attached.


Implementing a custom backend

To bridge purple_logger to a new OTel SDK, you need two classes and one function. This complete example uses a hypothetical your_otel_sdk package:

import 'package:your_otel_sdk/your_otel_sdk.dart';
import 'package:purple_logger/purple_logger.dart';
import 'package:purple_logger_otel/purple_logger_otel.dart';

// 1. The provider: creates logger instances and initializes the OTel SDK.
final class YourOtelLoggerProvider extends OtelLoggerProvider {
  final YourOtelSdk _sdk;

  YourOtelLoggerProvider(this._sdk);

  @override
  OtelLogger createOtelLogger(String category) =>
      _YourOtelLogger(category: category, sdk: _sdk);
}

// 2. The logger: mix in OtelSeverityMapping, implement the two abstract methods.
final class _YourOtelLogger implements OtelLogger, OtelSeverityMapping {
  @override
  final String category;

  final YourOtelSdk _sdk;

  _YourOtelLogger({required this.category, required YourOtelSdk sdk})
      : _sdk = sdk;

  @override
  void emitToOtel({
    required int severityNumber,
    required String severityText,
    required String body,
    required Map<String, Object> attributes,
  }) {
    _sdk.emitLogRecord(
      severityNumber: severityNumber,
      severityText: severityText,
      body: body,
      attributes: attributes,
    );
  }

  @override
  TraceContext extractTraceContext() {
    final span = _sdk.getActiveSpan();
    if (span == null) return TraceContext.empty;
    return TraceContext(
      traceId: span.traceId,
      spanId: span.spanId,
    );
  }

  // ── Convenience methods ──────────────────────────────────────────
  // Since your class implements Logger (via OtelLogger), you must also
  // provide the convenience methods that delegate to log(). Mix in
  // LoggerConvenience from purple_logger for this:
  @override
  void trace(Object? message, {Map<String, Object?>? properties}) =>
      log(PurpleLogLevel.trace, message, properties: properties);

  @override
  void debug(Object? message, {Map<String, Object?>? properties}) =>
      log(PurpleLogLevel.debug, message, properties: properties);

  @override
  void info(Object? message, {Map<String, Object?>? properties}) =>
      log(PurpleLogLevel.info, message, properties: properties);

  @override
  void warning(Object? message, {Map<String, Object?>? properties}) =>
      log(PurpleLogLevel.warning, message, properties: properties);

  @override
  void error(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
    Map<String, Object?>? properties,
  }) =>
      log(PurpleLogLevel.error, message,
          properties: properties, error: error, stackTrace: stackTrace);

  @override
  void fatal(
    Object? message, {
    Object? error,
    StackTrace? stackTrace,
    Map<String, Object?>? properties,
  }) =>
      log(PurpleLogLevel.fatal, message,
          properties: properties, error: error, stackTrace: stackTrace);

  @override
  void log(
    PurpleLogLevel level,
    Object? message, {
    Map<String, Object?>? properties,
    Object? error,
    StackTrace? stackTrace,
  }) {
    // Either implement log() yourself, or call super.log() with the
    // mixin logic from OtelSeverityMapping. The simplest path is to
    // override log() and replicate the OtelLogger base class logic
    // shown in the test file.
    super.log(level, message,
        properties: properties, error: error, stackTrace: stackTrace);
  }
}

// 3. Wire it up at startup:
void main() {
  final sdk = YourOtelSdk.initialize();
  final factory = LoggingBuilder()
      .addProvider(YourOtelLoggerProvider(sdk))
      .addConsole()
      .build();

  final logger = factory.createLogger('MyService');
  logger.info('Started', properties: {'version': '1.0.0'});
}

The only OTel-specific code lives in emitToOtel() and extractTraceContext(). Everything else — scope merging, error attachment, null filtering, severity mapping — is inherited from the base class.


Severity mapping

Follows the OpenTelemetry Logs Data Model.

PurpleLogLevel OTel SeverityNumber OTel SeverityText Notes
trace 1 (TRACE) TRACE Finest-grained diagnostics
debug 5 (DEBUG) DEBUG Developer diagnostics
info 9 (INFO) INFO Normal operational events
warning 13 (WARN) WARN Recoverable anomalies
error 17 (ERROR) ERROR Operation failures
fatal 21 (FATAL) FATAL Unrecoverable, requires intervention
none 0 (UNSPECIFIED) '' Log event suppressed

The mapping is available in three forms — use whichever fits your style:

// 1. As a mixin on any OtelLogger subclass
final class MyLogger implements OtelLogger, OtelSeverityMapping { ... }

// 2. As standalone functions
severityNumber(PurpleLogLevel.error);  // 17
severityText(PurpleLogLevel.error);    // 'ERROR'

// 3. Directly on PurpleLogLevel (built into purple_logger)
PurpleLogLevel.error.severityNumber;   // 17
PurpleLogLevel.error.severityText;     // 'ERROR'

Installation

This package depends only on purple_logger. It does not pull in any OpenTelemetry SDK.

# pubspec.yaml
dependencies:
  purple_logger: ^2.1.0
  purple_logger_otel: ^1.0.0
  # Choose one concrete bridge:
  purple_logger_otel_dartastic: ^1.0.0   # OR your custom bridge

Then import the abstractions:

import 'package:purple_logger/purple_logger.dart';
import 'package:purple_logger_otel/purple_logger_otel.dart';

Companion packages

Package Description pub.dev
purple_logger Core structured logger with provider pipeline, zone scopes, file rotation Pub
purple_logger_otel You are here — abstractions, severity mapping, trace context Pub
purple_logger_otel_dartastic Concrete bridge using dartastic_opentelemetry (planned)
purple_otel_sdk OTel SDK configuration, resource builders, batching exporters (planned)







Built by PurpleSoft

PurpleSoft S.r.l. — software house with offices in Monza, Milano, and Lugano (Switzerland). Since 2017.

We build what doesn't exist yet.

The sectors we dominate

Database & Storage Engines. We design, implement, and ship production-grade databases. Our LSM-tree storage engine — built in Rust for performance, exposed via FFI to Dart/Flutter, compiled to WASM for web — uses WiscKey-style key-value separation with concurrent compaction and crash recovery. It runs on all 6 platforms. This is not a wrapper around SQLite. It is a ground-up embedded NoSQL database written from scratch.

Conversational AI & Voice Assistants. We ship end-to-end AI voice platforms — from the physical device (ESP32 with custom Opus codec firmware) to the cloud backend (.NET 10, ASP.NET Core, Blazor Server) to the mobile companion app (Flutter with BLE). Our multi-agent LLM architecture orchestrates specialized agents (conversation, memory, content enrichment, research) with a multi-layered memory system spanning graph, episodic, and working memory — including adaptive forgetting, poison detection via statistical outlier analysis, and automatic episodic-to-semantic compression. Our content intelligence pipelines ingest, enrich via LLM, embed (1024-dim vectors), and serve via hybrid semantic search with HNSW indexing and training-free chunk pre-filtering — 28,000+ items enriched, 30,000+ embeddings generated.

Fintech & Payments. We build payment orchestration layers that abstract multiple gateways and cryptocurrencies behind a single API. Our engineers have shipped POS terminal firmware, fiscal receipt systems compliant with Italian regulatory standards, and cash register management platforms processing millions of transactions.

Cybersecurity & Identity. We ship post-quantum cryptography implementations using NIST-standardized algorithms on .NET 10 — the cryptography standard that will replace RSA and ECC. Our authentication infrastructure integrates SPID (Italian public digital identity) and OAuth 2.0/OpenID Connect across every major identity provider. We build digital signature platforms with PKCS#11 HSM support handling the full envelope lifecycle.

Artificial Intelligence & On-Device ML. We deploy ONNX models to phones via custom Dart runtime bindings with GPU acceleration. We integrate on-device LLM inference engines. We build neural text-to-speech engines that run across all 6 Flutter platforms and speech recognition systems with Italian dialect support. Our scientific research pipeline uses a multi-scorer verification chain with consensus voting to ensure factual accuracy in LLM outputs.

Enterprise SaaS & Cloud-Native Architecture. We architect and operate platforms at enterprise scale. Our .NET 10 monorepo spans 239 C# projects with consistent CI/CD, 297 test suites, zero build errors. We design multi-engine database abstraction layers (PostgreSQL, MySQL, Microsoft SQL Server) with automated schema-to-code generation. Our notification engine handles 6 channels with DNS-based email validation.

IoT & Embedded Systems. We write ESP32 firmware targeting ESP-IDF v5.4 with 21 FreeRTOS tasks, I²S audio pipelines, Opus codec integration, and a validated WiFi state machine. We build certificate authority infrastructure for device TLS. We write native Flutter plugins for hardware that doesn't have one yet.

Observability & DevOps. Full-stack observability is the foundation of our client solutions. We ship a complete OpenTelemetry SDK for Dart/Flutter (traces, logs, metrics, W3C propagation, OTLP export), enterprise structured logging with file rotation, and auto-instrumentation — all red-team audited with 256 automated tests.

The technologies we master

Our engineering team works across the full stack — from ESP32 firmware to cloud-native backends to cross-platform mobile apps. We write production code in Rust (database engines, FFI), C# (.NET 10), Dart/Flutter, C (ESP-IDF, audio codecs), TypeScript, Python, and C++. Our frameworks of choice are ASP.NET Core, Blazor Server, Flutter, Angular, and React. We operate Microsoft Azure (Key Vault, Blob Storage, IoT Hub), deploy on NGINX, and manage PostgreSQL + pgvector, MySQL, Microsoft SQL Server, and ESP-IDF at scale. Our database layer uses EF Core with Npgsql, our embedded engine uses Rust + dart:ffi + WASM. Our AI stack spans LLMs, embeddings, vector search, semantic kernel, and multi-agent orchestration. We use FFmpeg for audio, LVGL for embedded displays, and BLE for device provisioning. Our CI/CD runs on Azure Pipelines.

Microsoft Partner since 2018 · SumUp Partner · Dell Partner


Trusted by

ABB Intesa Sanpaolo Tenaris Reply Aubay Comune di Milano BCC FIMAP Alten Altran Prometeia illimity Be Shaping the Future DS Group NVALUE Inoptim Docflow P&C

and 40+ other enterprises across banking, manufacturing, energy, and public sector.


Your project can't wait. We've solved these exact problems for companies you know. Let's solve them for you.

🌐 purplesoft.io  ·  📧 developers@purplesoft.io  ·  📞 +39 0362 148 3978  ·  💼 LinkedIn  ·  🐙 GitHub

License

AGPL-3.0 — see LICENSE for details.

About

PurpleOTel — OpenTelemetry SDK for Dart/Flutter

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages