From 0f3f924c03d4ffdd92c24b39a4878bf6d1b6d956 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 13 May 2026 16:44:33 -0700 Subject: [PATCH 1/8] chore: use google_cloud_(shelf/logging) --- lib/logger.dart | 22 +- lib/src/https/callable.dart | 5 +- lib/src/logger/logger.dart | 273 +----------------------- lib/src/server.dart | 62 ++---- pubspec.yaml | 2 + test/unit/logger_test.dart | 409 ------------------------------------ test/unit/server_test.dart | 33 --- 7 files changed, 36 insertions(+), 770 deletions(-) delete mode 100644 test/unit/logger_test.dart diff --git a/lib/logger.dart b/lib/logger.dart index d45b82a..2ddd8dc 100644 --- a/lib/logger.dart +++ b/lib/logger.dart @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// Structured logger for Cloud Logging, compatible with the Firebase -/// Functions Node.js SDK `logger` namespace. +/// Structured logger for Cloud Logging. /// /// ## Usage /// @@ -21,37 +20,26 @@ /// import 'package:firebase_functions/logger.dart'; /// /// logger.info('Request received'); -/// logger.warn('Slow query', {'durationMs': 1200, 'query': 'SELECT ...'}); +/// logger.warning('Slow query', payload: {'durationMs': 1200, 'query': 'SELECT ...'}); /// logger.error('Failed to process request'); /// ``` /// /// ## Structured Logging /// -/// Pass a [Map] as the second argument to include +/// Pass a [Map] using the named `payload:` argument to include /// structured data in the Cloud Logging `jsonPayload`: /// /// ```dart -/// logger.info('User signed in', { +/// logger.info('User signed in', payload: { /// 'userId': user.id, /// 'provider': 'google', /// }); /// ``` /// -/// Or pass a [Map] as the sole argument for structured-only entries: -/// -/// ```dart -/// logger.info({'message': 'Batch complete', 'processedCount': 42}); -/// ``` -/// /// ## Severity Routing /// /// - **stdout**: DEBUG, INFO, NOTICE /// - **stderr**: WARNING, ERROR, CRITICAL, ALERT, EMERGENCY library; -export 'src/logger/logger.dart' - hide - cloudTraceContextHeader, - createLogger, - projectIdZoneKey, - traceIdZoneKey; +export 'src/logger/logger.dart'; diff --git a/lib/src/https/callable.dart b/lib/src/https/callable.dart index a78c47d..23ed879 100644 --- a/lib/src/https/callable.dart +++ b/lib/src/https/callable.dart @@ -264,7 +264,10 @@ class CallableResponse { unawaited(sendChunk(result.data)); }, onError: (Object error) { - logger.error('Error in data stream', {'error': error.toString()}); + logger.error( + 'Error in data stream', + payload: {'error': error.toString()}, + ); if (error is HttpsError) { writeSSE(error.toErrorResponse()); } else { diff --git a/lib/src/logger/logger.dart b/lib/src/logger/logger.dart index aa96100..4d853b2 100644 --- a/lib/src/logger/logger.dart +++ b/lib/src/logger/logger.dart @@ -12,273 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; +import 'package:google_cloud_logging/google_cloud_logging.dart'; +import 'package:google_cloud_shelf/google_cloud_shelf.dart'; -import 'package:meta/meta.dart'; - -/// Log severity levels for Cloud Logging. -/// -/// See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity). -enum LogSeverity { - debug('DEBUG'), - info('INFO'), - notice('NOTICE'), - warning('WARNING'), - error('ERROR'), - critical('CRITICAL'), - alert('ALERT'), - emergency('EMERGENCY'); - - const LogSeverity(this.value); - - /// The string value used in Cloud Logging JSON entries. - final String value; -} - -/// A structured Cloud Logging log entry. -/// -/// A [Map] where `severity` (required) and `message` (optional) are standard -/// Cloud Logging fields. All other keys are included in the `jsonPayload` -/// of the logged entry. -typedef LogEntry = Map; - -/// Removes circular references from an object graph for safe JSON -/// serialization. -/// -/// Returns a new data structure with circular references replaced by the -/// string `"[Circular]"`. Does not mutate the original object. -/// -/// Handles [DateTime] by converting to ISO 8601 UTC string and respects -/// objects with a `toJson()` method. -Object? removeCircular(Object? obj, [Set? existingRefs]) { - if (obj == null || obj is bool || obj is num || obj is String) { - return obj; - } - - if (obj is DateTime) { - return obj.toUtc().toIso8601String(); - } - - final refs = existingRefs ?? {}; - - // Handle objects with toJson() (custom serializable types). - if (obj is! Map && obj is! List) { - try { - final result = (obj as dynamic).toJson(); - return removeCircular(result, refs); - } catch (_) { - return obj.toString(); - } - } - - if (refs.contains(obj)) { - return '[Circular]'; - } - refs.add(obj); - - late final Object? result; - - if (obj is Map) { - final map = {}; - for (final MapEntry(:key, :value) in obj.entries) { - try { - if (value != null && refs.contains(value)) { - map[key.toString()] = '[Circular]'; - } else { - map[key.toString()] = removeCircular(value, refs); - } - } catch (_) { - map[key.toString()] = '[Error - cannot serialize]'; - } - } - result = map; - } else { - // obj is List - result = List.generate((obj as List).length, (i) { - final value = obj[i]; - try { - if (value != null && refs.contains(value)) { - return '[Circular]'; - } - return removeCircular(value, refs); - } catch (_) { - return '[Error - cannot serialize]'; - } - }); - } - - refs.remove(obj); - return result; -} - -/// Whether a severity level should be written to stderr. -bool _isStderrSeverity(String severity) => switch (severity) { - 'WARNING' || 'ERROR' || 'CRITICAL' || 'ALERT' || 'EMERGENCY' => true, - _ => false, -}; - -/// Creates a new [Logger] instance. -/// -/// [stdoutWriter] and [stderrWriter] can be provided for testing. -@internal -Logger createLogger({ - void Function(String line)? stdoutWriter, - void Function(String line)? stderrWriter, -}) => Logger._(stdoutWriter: stdoutWriter, stderrWriter: stderrWriter); - -/// Structured logger for Cloud Logging, compatible with the Firebase -/// Functions Node.js SDK `logger` namespace. -/// -/// Writes JSON-formatted [LogEntry] objects to stdout or stderr depending -/// on severity. DEBUG, INFO, and NOTICE go to stdout; WARNING, ERROR, -/// CRITICAL, ALERT, and EMERGENCY go to stderr. -/// -/// ## Usage -/// -/// ```dart -/// import 'package:firebase_functions/logger.dart'; -/// -/// // Simple message -/// logger.info('Request received'); -/// -/// // Message with structured data -/// logger.info('Processing', {'userId': '123', 'action': 'update'}); -/// -/// // Structured data only (message inside the map is preserved) -/// logger.info({'message': 'Custom', 'requestId': 'abc'}); -/// -/// // Low-level structured entry -/// logger.write({'severity': 'NOTICE', 'message': 'Custom', 'code': 42}); -/// ``` -final class Logger { - /// Creates a [Logger] instance. - /// - /// Custom [stdoutWriter] and [stderrWriter] can be provided for testing. - Logger._({ - void Function(String line)? stdoutWriter, - void Function(String line)? stderrWriter, - }) : _stdoutWriter = stdoutWriter ?? _defaultStdoutWriter, - _stderrWriter = stderrWriter ?? _defaultStderrWriter; - - final void Function(String line) _stdoutWriter; - final void Function(String line) _stderrWriter; - - static void _defaultStdoutWriter(String line) => io.stdout.writeln(line); - static void _defaultStderrWriter(String line) => io.stderr.writeln(line); - - /// Writes a [LogEntry] to stdout or stderr depending on severity. - /// - /// The entry must contain a `severity` key. If a trace ID is available - /// in the current [Zone] (via [traceIdZoneKey]), it is automatically added - /// to the entry. - void write(LogEntry entry) { - // Add trace context if available. - - final projectId = Zone.current[projectIdZoneKey] as String?; - final traceId = Zone.current[traceIdZoneKey] as String?; - - if (projectId != null && traceId != null) { - assert(projectId.isNotEmpty, 'projectIdZoneKey value must not be empty'); - assert(traceId.isNotEmpty, 'traceIdZoneKey value must not be empty'); - entry['logging.googleapis.com/trace'] = - 'projects/$projectId/traces/$traceId'; - } - - final sanitized = removeCircular(entry); - final json = jsonEncode(sanitized); - - final severity = entry['severity'] as String? ?? 'INFO'; - if (_isStderrSeverity(severity)) { - _stderrWriter(json); - } else { - _stdoutWriter(json); - } - } - - /// Writes a DEBUG severity log. - /// - /// If [messageOrPayload] is a [Map] and [jsonPayload] - /// is null, the map is used directly as the structured log entry - /// (preserving any `message` key within it). - /// - /// Otherwise, [messageOrPayload] is converted to a string for the - /// `message` field, and [jsonPayload] entries are merged into the entry. - void debug(Object? messageOrPayload, [Map? jsonPayload]) { - write(_entryFromArgs('DEBUG', messageOrPayload, jsonPayload)); - } - - /// Writes an INFO severity log. Alias for [info]. - void log(Object? messageOrPayload, [Map? jsonPayload]) { - write(_entryFromArgs('INFO', messageOrPayload, jsonPayload)); - } - - /// Writes an INFO severity log. - void info(Object? messageOrPayload, [Map? jsonPayload]) { - write(_entryFromArgs('INFO', messageOrPayload, jsonPayload)); - } - - /// Writes a WARNING severity log. - void warn(Object? messageOrPayload, [Map? jsonPayload]) { - write(_entryFromArgs('WARNING', messageOrPayload, jsonPayload)); - } - - /// Writes an ERROR severity log. - void error(Object? messageOrPayload, [Map? jsonPayload]) { - write(_entryFromArgs('ERROR', messageOrPayload, jsonPayload)); - } -} - -/// Constructs a [LogEntry] from a severity, message, and optional JSON -/// payload. -/// -/// When [messageOrPayload] is a [Map] and [jsonPayload] -/// is null, the map is used directly as structured data (matching Node.js -/// behavior where a lone plain-object argument is treated as the entry). -/// -/// Otherwise, [messageOrPayload] is stringified and set as the `message` -/// field, overwriting any `message` key from [jsonPayload]. -LogEntry _entryFromArgs( - String severity, - Object? messageOrPayload, - Map? jsonPayload, -) { - // If only a Map was passed, treat it as structured data. - if (messageOrPayload is Map && jsonPayload == null) { - return {...messageOrPayload, 'severity': severity}; - } - - final entry = {...?jsonPayload, 'severity': severity}; - - final messageStr = '$messageOrPayload'; - if (messageStr.isNotEmpty) { - entry['message'] = messageStr; - } - - return entry; -} - -/// Default [Logger] instance. -/// -/// This is the primary way to use the logger: -/// ```dart -/// import 'package:firebase_functions/logger.dart'; -/// -/// logger.info('Hello'); -/// logger.warn('Something is off', {'requestId': 'abc'}); -/// ``` -final logger = Logger._(); - -/// Standard HTTP header used by -/// [Cloud Trace](https://cloud.google.com/trace/docs/setup). -@internal -const cloudTraceContextHeader = 'x-cloud-trace-context'; - -/// Zone key for propagating trace IDs. -@internal -final Object traceIdZoneKey = Object(); - -/// Zone key for propagating project ID. -@internal -final Object projectIdZoneKey = Object(); +/// The default instance used for logging in the current context. +CloudLogger get logger => currentLogger; diff --git a/lib/src/server.dart b/lib/src/server.dart index dde8d12..112bd5c 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -16,9 +16,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:google_cloud_shelf/google_cloud_shelf.dart'; import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart'; -import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:stack_trace/stack_trace.dart' show Trace; import 'common/cloud_run_id.dart'; @@ -69,34 +69,31 @@ Future runFunctions(FunctionsRunner runner) async { final env = firebase.$env; final projectId = env.projectId; - await runZoned(zoneValues: {projectIdZoneKey: projectId}, () async { - // Run user's function registration code - await runner(firebase); + // Run user's function registration code + await runner(firebase); - // Build request handler with middleware pipeline - var middleware = const Pipeline().middleware; + // Build request handler with middleware pipeline + var middleware = const Pipeline().middleware; - final env = firebase.$env; - if (env.enableCors) { - middleware = middleware.addMiddleware(_corsMiddleware); - } - - // Build request handler with middleware pipeline - final handler = middleware.addHandler((request) { - final traceId = extractTraceId(request.headers[cloudTraceContextHeader]); + if (env.enableCors) { + middleware = middleware.addMiddleware(_corsMiddleware); + } - if (traceId == null) { - return _routeRequest(request, firebase, env); - } + if (env.isEmulator) { + middleware = middleware.addMiddleware(logRequests()); + } - return runZoned(zoneValues: {traceIdZoneKey: traceId}, () { - return _routeRequest(request, firebase, env); - }); - }); + middleware = middleware.addMiddleware( + createLoggingMiddleware(projectId: projectId), + ); - // Start HTTP server - await shelf_io.serve(handler, InternetAddress.anyIPv4, env.port); + // Build request handler with middleware pipeline + final handler = middleware.addHandler((request) { + return _routeRequest(request, firebase, env); }); + + // Start HTTP server + await serveHandler(handler); } /// CORS middleware for emulator mode. @@ -525,7 +522,7 @@ Future<(Request, FirebaseFunctionDeclaration?)> _tryMatchCloudEventFunction( return (finalRequest, null); } catch (e, stackTrace) { // CloudEvent parsing failed - not a CloudEvent request - logger.warn( + logger.warning( 'CloudEvent parsing failed: $e\n${Trace.from(stackTrace).terse}', ); return (request, null); @@ -738,20 +735,3 @@ bool _matchesRefPattern(String refPath, String pattern) { return true; } -final _traceIdRegExp = RegExp(r'^[a-f0-9]{32}$', caseSensitive: false); - -/// Extracts the 32-character hexadecimal trace ID from an [x-cloud-trace-context] header. -/// -/// Expected format: `TRACE_ID/SPAN_ID;o=TRACE_TRUE` -@visibleForTesting -String? extractTraceId(String? header) { - if (header == null || header.isEmpty) return null; - final parts = header.split('/'); - if (parts.isNotEmpty) { - final traceId = parts[0]; - if (_traceIdRegExp.hasMatch(traceId)) { - return traceId; - } - } - return null; -} diff --git a/pubspec.yaml b/pubspec.yaml index 7d37599..80bc987 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: firebase_admin_sdk: ^0.5.0 glob: ^2.1.3 google_cloud_firestore: ^0.5.0 + google_cloud_logging: ^0.5.0 + google_cloud_shelf: ^0.5.0 http: ^1.6.0 meta: ^1.17.0 protobuf: ^6.0.0 diff --git a/test/unit/logger_test.dart b/test/unit/logger_test.dart deleted file mode 100644 index 3071c32..0000000 --- a/test/unit/logger_test.dart +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// ignore_for_file: avoid_dynamic_calls - -import 'dart:async'; -import 'dart:convert'; - -import 'package:firebase_functions/src/logger/logger.dart'; -import 'package:test/test.dart'; - -void main() { - group('Logger', () { - late String lastStdout; - late String lastStderr; - late Logger testLogger; - - setUp(() { - lastStdout = ''; - lastStderr = ''; - testLogger = createLogger( - stdoutWriter: (line) => lastStdout = line, - stderrWriter: (line) => lastStderr = line, - ); - }); - - Map parseStdout() => - jsonDecode(lastStdout) as Map; - - Map parseStderr() => - jsonDecode(lastStderr) as Map; - - group('logging methods', () { - test('should write message to output', () { - testLogger.log('hello world'); - expect(parseStdout(), {'severity': 'INFO', 'message': 'hello world'}); - }); - - test('should merge structured data from jsonPayload', () { - testLogger.log('hello world', {'additional': 'context'}); - expect(parseStdout(), { - 'severity': 'INFO', - 'message': 'hello world', - 'additional': 'context', - }); - }); - - test('should handle null message', () { - testLogger.log(null); - expect(parseStdout(), {'severity': 'INFO', 'message': 'null'}); - }); - - test('should overwrite message field in structured data when ' - 'message is provided', () { - testLogger.log('this instead', {'test': true, 'message': 'not this'}); - expect(parseStdout(), { - 'severity': 'INFO', - 'message': 'this instead', - 'test': true, - }); - }); - - test('should not overwrite message field when only structured data ' - 'is provided', () { - testLogger.log({'test': true, 'message': 'this'}); - expect(parseStdout(), { - 'severity': 'INFO', - 'message': 'this', - 'test': true, - }); - }); - - test('should handle structured data without message', () { - testLogger.log({'test': true, 'count': 42}); - expect(parseStdout(), {'severity': 'INFO', 'test': true, 'count': 42}); - }); - - test('should handle empty string message', () { - testLogger.log(''); - expect(parseStdout(), {'severity': 'INFO'}); - }); - }); - - group('severity methods', () { - test('debug writes DEBUG severity to stdout', () { - testLogger.debug('test'); - expect(parseStdout(), {'severity': 'DEBUG', 'message': 'test'}); - }); - - test('info writes INFO severity to stdout', () { - testLogger.info('test'); - expect(parseStdout(), {'severity': 'INFO', 'message': 'test'}); - }); - - test('log writes INFO severity to stdout', () { - testLogger.log('test'); - expect(parseStdout(), {'severity': 'INFO', 'message': 'test'}); - }); - - test('warn writes WARNING severity to stderr', () { - testLogger.warn('test'); - expect(parseStderr(), {'severity': 'WARNING', 'message': 'test'}); - }); - - test('error writes ERROR severity to stderr', () { - testLogger.error('test'); - expect(parseStderr(), {'severity': 'ERROR', 'message': 'test'}); - }); - }); - - group('severity methods with structured data', () { - test('debug with jsonPayload', () { - testLogger.debug('msg', {'key': 'value'}); - expect(parseStdout(), { - 'severity': 'DEBUG', - 'message': 'msg', - 'key': 'value', - }); - }); - - test('warn with jsonPayload', () { - testLogger.warn('msg', {'key': 'value'}); - expect(parseStderr(), { - 'severity': 'WARNING', - 'message': 'msg', - 'key': 'value', - }); - }); - - test('error with jsonPayload', () { - testLogger.error('msg', {'key': 'value'}); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'msg', - 'key': 'value', - }); - }); - }); - - group('write', () { - test('should remove circular references', () { - final circ = {'b': 'foo'}; - circ['circ'] = circ; - - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing circular', - 'circ': circ, - }); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'testing circular', - 'circ': {'b': 'foo', 'circ': '[Circular]'}, - }); - }); - - test('should remove circular references in arrays', () { - final circ = {'b': 'foo'}; - circ['circ'] = [circ]; - - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing circular', - 'circ': circ, - }); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'testing circular', - 'circ': { - 'b': 'foo', - 'circ': ['[Circular]'], - }, - }); - }); - - test('should not detect duplicate object as circular', () { - final obj = {'a': 'foo'}; - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing circular', - 'a': obj, - 'b': obj, - }); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'testing circular', - 'a': {'a': 'foo'}, - 'b': {'a': 'foo'}, - }); - }); - - test('should not detect duplicate object in array as circular', () { - final obj = {'a': 'foo'}; - final arr = [ - {'a': obj, 'b': obj}, - {'a': obj, 'b': obj}, - ]; - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing circular', - 'a': arr, - 'b': arr, - }); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'testing circular', - 'a': [ - { - 'a': {'a': 'foo'}, - 'b': {'a': 'foo'}, - }, - { - 'a': {'a': 'foo'}, - 'b': {'a': 'foo'}, - }, - ], - 'b': [ - { - 'a': {'a': 'foo'}, - 'b': {'a': 'foo'}, - }, - { - 'a': {'a': 'foo'}, - 'b': {'a': 'foo'}, - }, - ], - }); - }); - - test('should handle objects with toJson()', () { - final date = DateTime.utc(1994, 8, 26, 12, 24); - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing toJSON', - 'obj': {'a': date}, - }); - expect(parseStderr(), { - 'severity': 'ERROR', - 'message': 'testing toJSON', - 'obj': {'a': '1994-08-26T12:24:00.000Z'}, - }); - }); - - test('should not alter parameters that are logged', () { - final circ = {'b': 'foo'}; - circ['array'] = [circ]; - circ['object'] = circ; - - testLogger.write({ - 'severity': 'ERROR', - 'message': 'testing circular', - 'circ': circ, - }); - - // Verify original object is not mutated. - expect(circ['b'], 'foo'); - expect((circ['object'] as Map)['b'], 'foo'); - expect( - ((circ['object'] as Map)['array'] as List)[0]['object'] is Map, - isTrue, - ); - }); - - for (final severity in ['DEBUG', 'INFO', 'NOTICE']) { - test('should output $severity severity to stdout', () { - testLogger.write({'severity': severity, 'message': 'test'}); - expect(parseStdout(), {'severity': severity, 'message': 'test'}); - }); - } - - for (final severity in [ - 'WARNING', - 'ERROR', - 'CRITICAL', - 'ALERT', - 'EMERGENCY', - ]) { - test('should output $severity severity to stderr', () { - testLogger.write({'severity': severity, 'message': 'test'}); - expect(parseStderr(), {'severity': severity, 'message': 'test'}); - }); - } - }); - - group('trace context', () { - test( - 'should add trace header when both projectId and traceId are in Zone', - () { - runZoned( - () { - testLogger.write({'severity': 'INFO', 'message': 'traced'}); - expect(parseStdout(), { - 'severity': 'INFO', - 'message': 'traced', - 'logging.googleapis.com/trace': - 'projects/test-project/traces/abc123', - }); - }, - zoneValues: { - projectIdZoneKey: 'test-project', - traceIdZoneKey: 'abc123', - }, - ); - }, - ); - - test('should not add trace header when projectId is missing in Zone', () { - runZoned(() { - testLogger.write({'severity': 'INFO', 'message': 'traced'}); - expect(parseStdout(), {'severity': 'INFO', 'message': 'traced'}); - }, zoneValues: {traceIdZoneKey: 'abc123'}); - }); - - test('should not add trace header when traceId is missing in Zone', () { - runZoned(() { - testLogger.write({'severity': 'INFO', 'message': 'traced'}); - expect(parseStdout(), {'severity': 'INFO', 'message': 'traced'}); - }, zoneValues: {projectIdZoneKey: 'test-project'}); - }); - }); - }); - - group('removeCircular', () { - test('should return primitives as-is', () { - expect(removeCircular(null), isNull); - expect(removeCircular(true), true); - expect(removeCircular(42), 42); - expect(removeCircular(3.14), 3.14); - expect(removeCircular('hello'), 'hello'); - }); - - test('should handle DateTime', () { - final date = DateTime.utc(2024, 1, 15, 10, 30); - expect(removeCircular(date), '2024-01-15T10:30:00.000Z'); - }); - - test('should handle simple maps', () { - expect(removeCircular({'a': 1, 'b': 'hello'}), {'a': 1, 'b': 'hello'}); - }); - - test('should handle simple lists', () { - expect(removeCircular([1, 2, 3]), [1, 2, 3]); - }); - - test('should handle nested structures', () { - expect( - removeCircular({ - 'a': { - 'b': [1, 2, 3], - }, - }), - { - 'a': { - 'b': [1, 2, 3], - }, - }, - ); - }); - - test('should replace circular map reference', () { - final map = {'key': 'value'}; - map['self'] = map; - - expect(removeCircular(map), {'key': 'value', 'self': '[Circular]'}); - }); - - test('should replace circular list reference', () { - final list = ['value']; - list.add(list); - - expect(removeCircular(list), ['value', '[Circular]']); - }); - - test('should handle deeply nested circular references', () { - final a = {'name': 'a'}; - final b = {'name': 'b', 'parent': a}; - a['child'] = b; - - expect(removeCircular(a), { - 'name': 'a', - 'child': {'name': 'b', 'parent': '[Circular]'}, - }); - }); - }); - - group('LogSeverity', () { - test('should have correct string values', () { - expect(LogSeverity.debug.value, 'DEBUG'); - expect(LogSeverity.info.value, 'INFO'); - expect(LogSeverity.notice.value, 'NOTICE'); - expect(LogSeverity.warning.value, 'WARNING'); - expect(LogSeverity.error.value, 'ERROR'); - expect(LogSeverity.critical.value, 'CRITICAL'); - expect(LogSeverity.alert.value, 'ALERT'); - expect(LogSeverity.emergency.value, 'EMERGENCY'); - }); - }); -} diff --git a/test/unit/server_test.dart b/test/unit/server_test.dart index c881b5b..f6637ad 100644 --- a/test/unit/server_test.dart +++ b/test/unit/server_test.dart @@ -18,39 +18,6 @@ import 'package:test/test.dart'; void main() { group('server', () { - group('extractTraceId', () { - test('extracts valid trace ID with span and option', () { - const header = '4bf92f3577b34da6a3ce929d0e0e4736/12345;o=1'; - expect(extractTraceId(header), '4bf92f3577b34da6a3ce929d0e0e4736'); - }); - - test('extracts valid trace ID with uppercase hex', () { - const header = '4BF92F3577B34DA6A3CE929D0E0E4736/12345;o=1'; - expect(extractTraceId(header), '4BF92F3577B34DA6A3CE929D0E0E4736'); - }); - - test('extracts valid trace ID without span or option', () { - const header = '1234567890abcdef1234567890abcdef'; - expect(extractTraceId(header), '1234567890abcdef1234567890abcdef'); - }); - - test('handles null and empty', () { - expect(extractTraceId(null), isNull); - expect(extractTraceId(''), isNull); - }); - - test('rejects malformed traces', () { - // Too short - expect(extractTraceId('1234/567;o=1'), isNull); - - // Too long - expect(extractTraceId('1234567890abcdef1234567890abcdef0/5'), isNull); - - // Invalid hex - expect(extractTraceId('1234567890xyzdef1234567890abcdef/5'), isNull); - }); - }); - group('corsHeadersFor', () { test('returns asterisk when allowedOrigins contains asterisk', () { final request = Request('GET', Uri.parse('http://localhost/test')); From eb7f4b19cfd1e70e33dcad5637df496cd50405d3 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 13 May 2026 17:15:05 -0700 Subject: [PATCH 2/8] remove request logging --- lib/src/server.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/server.dart b/lib/src/server.dart index 112bd5c..3e4e012 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -79,10 +79,6 @@ Future runFunctions(FunctionsRunner runner) async { middleware = middleware.addMiddleware(_corsMiddleware); } - if (env.isEmulator) { - middleware = middleware.addMiddleware(logRequests()); - } - middleware = middleware.addMiddleware( createLoggingMiddleware(projectId: projectId), ); From bf086c40e5594dadc4637087c328897775e00b3a Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 13 May 2026 17:32:25 -0700 Subject: [PATCH 3/8] update versions --- CHANGELOG.md | 5 +++++ example/alerts/pubspec.yaml | 2 +- example/auth/pubspec.yaml | 2 +- example/database/pubspec.yaml | 2 +- example/eventarc/pubspec.yaml | 2 +- example/firestore/pubspec.yaml | 2 +- example/firestore_test/pubspec.yaml | 2 +- example/https/pubspec.yaml | 2 +- example/pubsub/pubspec.yaml | 2 +- example/remoteconfig/pubspec.yaml | 2 +- example/scheduler/pubspec.yaml | 2 +- example/storage/pubspec.yaml | 2 +- example/tasks/pubspec.yaml | 2 +- example/testlab/pubspec.yaml | 2 +- pubspec.yaml | 2 +- test/fixtures/dart_reference/pubspec.yaml | 2 +- 16 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73861eb..9444333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.7.0-wip + +- **BREAKING:** Replace logging implementation with + [`package:google_cloud_logging`](https://pub.dev/packages/google_cloud_logging). + ## 0.6.0 - Add `runFunctions` as the primary API. diff --git a/example/alerts/pubspec.yaml b/example/alerts/pubspec.yaml index b9e9d69..3849b05 100644 --- a/example/alerts/pubspec.yaml +++ b/example/alerts/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/auth/pubspec.yaml b/example/auth/pubspec.yaml index 7a93e22..b1aee7d 100644 --- a/example/auth/pubspec.yaml +++ b/example/auth/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/database/pubspec.yaml b/example/database/pubspec.yaml index 65d8d53..f022240 100644 --- a/example/database/pubspec.yaml +++ b/example/database/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/eventarc/pubspec.yaml b/example/eventarc/pubspec.yaml index e57267b..e54b604 100644 --- a/example/eventarc/pubspec.yaml +++ b/example/eventarc/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/firestore/pubspec.yaml b/example/firestore/pubspec.yaml index cdf98c5..7b509ce 100644 --- a/example/firestore/pubspec.yaml +++ b/example/firestore/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/firestore_test/pubspec.yaml b/example/firestore_test/pubspec.yaml index 7cd3a1f..08b1b77 100644 --- a/example/firestore_test/pubspec.yaml +++ b/example/firestore_test/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/https/pubspec.yaml b/example/https/pubspec.yaml index 39a3a45..f01eec6 100644 --- a/example/https/pubspec.yaml +++ b/example/https/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/pubsub/pubspec.yaml b/example/pubsub/pubspec.yaml index dc35fdf..893c359 100644 --- a/example/pubsub/pubspec.yaml +++ b/example/pubsub/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/remoteconfig/pubspec.yaml b/example/remoteconfig/pubspec.yaml index 5a005b1..9d56d84 100644 --- a/example/remoteconfig/pubspec.yaml +++ b/example/remoteconfig/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/scheduler/pubspec.yaml b/example/scheduler/pubspec.yaml index 5530a7f..9689d1b 100644 --- a/example/scheduler/pubspec.yaml +++ b/example/scheduler/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/storage/pubspec.yaml b/example/storage/pubspec.yaml index d1a78b8..f8a252d 100644 --- a/example/storage/pubspec.yaml +++ b/example/storage/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/tasks/pubspec.yaml b/example/tasks/pubspec.yaml index 9608ba6..2bf014e 100644 --- a/example/tasks/pubspec.yaml +++ b/example/tasks/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/example/testlab/pubspec.yaml b/example/testlab/pubspec.yaml index 87f9fff..7066933 100644 --- a/example/testlab/pubspec.yaml +++ b/example/testlab/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.7.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 diff --git a/pubspec.yaml b/pubspec.yaml index 80bc987..252d0d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ name: firebase_functions description: >- Cloud Functions for Dart with support for the Firebase Admin SDK, Cloud Storage and Firestore -version: 0.6.0 +version: 0.7.0-wip repository: https://github.com/firebase/firebase-functions-dart environment: diff --git a/test/fixtures/dart_reference/pubspec.yaml b/test/fixtures/dart_reference/pubspec.yaml index a230347..0f0ae3b 100644 --- a/test/fixtures/dart_reference/pubspec.yaml +++ b/test/fixtures/dart_reference/pubspec.yaml @@ -22,7 +22,7 @@ environment: sdk: ^3.9.0 dependencies: - firebase_functions: ^0.6.0-0 + firebase_functions: ^0.7.0-0 dev_dependencies: build_runner: ^2.10.5 From c3ec1d3797462aa4ff6f1d7c30fb54b849e8d0fa Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 13 May 2026 18:45:08 -0700 Subject: [PATCH 4/8] Update server.dart --- lib/src/server.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/server.dart b/lib/src/server.dart index 3e4e012..1f887ad 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -89,6 +89,9 @@ Future runFunctions(FunctionsRunner runner) async { }); // Start HTTP server + // XXX respect `env.port`! + // What about the signal handling stuff? I think that should be fine + // locally. await serveHandler(handler); } From ff4159a5779449b7ac9433155e6f342c63273cf1 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 29 May 2026 14:00:00 -0700 Subject: [PATCH 5/8] fix --- CHANGELOG.md | 6 +++-- example/https/bin/server.dart | 2 ++ lib/firebase_functions.dart | 2 -- lib/logger.dart | 7 +---- lib/src/common/utilities.dart | 2 +- lib/src/https/callable.dart | 3 ++- lib/src/logger/logger.dart | 41 +++++++++++++++++++++++++++--- lib/src/server.dart | 1 + lib/src/tasks/tasks_namespace.dart | 1 + pubspec.yaml | 6 +++++ 10 files changed, 56 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9444333..db8e59d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## 0.7.0-wip -- **BREAKING:** Replace logging implementation with - [`package:google_cloud_logging`](https://pub.dev/packages/google_cloud_logging). +- **BREAKING:** Remove the `logger` field from `logger.dart` and made its + method functions. +- **BREAKING:** Remove the `logger` exports from + `package:firebase_functions/firebase_functions.dart`. ## 0.6.0 diff --git a/example/https/bin/server.dart b/example/https/bin/server.dart index 81ed241..3afc1de 100644 --- a/example/https/bin/server.dart +++ b/example/https/bin/server.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'package:firebase_functions/firebase_functions.dart'; +import 'package:firebase_functions/logger.dart'; // Define parameters - these are read from environment variables at runtime // and can be configured at deploy time via .env files or CLI prompts. @@ -122,6 +123,7 @@ void main(List args) async { // ignore: non_const_argument_for_const_parameter options: HttpsOptions(minInstances: DeployOption.param(minInstances)), (request) async { + error('This is a request', {'something': 'else'}); return Response.ok(welcomeMessage.value()); }, ); diff --git a/lib/firebase_functions.dart b/lib/firebase_functions.dart index fa6c873..1bbcffe 100644 --- a/lib/firebase_functions.dart +++ b/lib/firebase_functions.dart @@ -114,8 +114,6 @@ export 'src/firestore/firestore.dart'; export 'src/https/https.dart'; // Experimental: Identity triggers (not yet supported in production or emulator) export 'src/identity/identity.dart'; -// Logger -export 'src/logger/logger.dart' show LogEntry, LogSeverity, Logger, logger; // Experimental: Pub/Sub triggers (not yet supported in production or emulator) export 'src/pubsub/pubsub.dart'; // Experimental: Remote Config triggers (not yet supported in production or emulator) diff --git a/lib/logger.dart b/lib/logger.dart index 2ddd8dc..9ed5080 100644 --- a/lib/logger.dart +++ b/lib/logger.dart @@ -17,7 +17,7 @@ /// ## Usage /// /// ```dart -/// import 'package:firebase_functions/logger.dart'; +/// import 'package:firebase_functions/logger.dart' as logger; /// /// logger.info('Request received'); /// logger.warning('Slow query', payload: {'durationMs': 1200, 'query': 'SELECT ...'}); @@ -35,11 +35,6 @@ /// 'provider': 'google', /// }); /// ``` -/// -/// ## Severity Routing -/// -/// - **stdout**: DEBUG, INFO, NOTICE -/// - **stderr**: WARNING, ERROR, CRITICAL, ALERT, EMERGENCY library; export 'src/logger/logger.dart'; diff --git a/lib/src/common/utilities.dart b/lib/src/common/utilities.dart index 2525ee4..c256efb 100644 --- a/lib/src/common/utilities.dart +++ b/lib/src/common/utilities.dart @@ -17,8 +17,8 @@ import 'dart:convert'; import 'package:shelf/shelf.dart' show Request, Response; import 'package:stack_trace/stack_trace.dart' show Trace; +import '../../logger.dart' as logger; import '../https/error.dart'; -import '../logger/logger.dart'; Future> readAsJsonMap(Request request) async { final decoded = await _converter.bind(request.read()).first; diff --git a/lib/src/https/callable.dart b/lib/src/https/callable.dart index 23ed879..195538b 100644 --- a/lib/src/https/callable.dart +++ b/lib/src/https/callable.dart @@ -19,6 +19,7 @@ import 'dart:typed_data'; import 'package:shelf/shelf.dart'; +import '../../logger.dart' as logger; import '../logger/logger.dart'; import 'error.dart'; @@ -266,7 +267,7 @@ class CallableResponse { onError: (Object error) { logger.error( 'Error in data stream', - payload: {'error': error.toString()}, + {'error': error.toString()}, ); if (error is HttpsError) { writeSSE(error.toErrorResponse()); diff --git a/lib/src/logger/logger.dart b/lib/src/logger/logger.dart index 4d853b2..4047923 100644 --- a/lib/src/logger/logger.dart +++ b/lib/src/logger/logger.dart @@ -13,7 +13,42 @@ // limitations under the License. import 'package:google_cloud_logging/google_cloud_logging.dart'; -import 'package:google_cloud_shelf/google_cloud_shelf.dart'; -/// The default instance used for logging in the current context. -CloudLogger get logger => currentLogger; +const _logger = StructuredLogger(); + +void write( + LogSeverity severity, + String message, [ + Map payload = const {}, + StackTrace? stackTrace, +]) { + final copy = {...payload}; + if (message.isNotEmpty) { + copy['message'] = message; + } + _logger.log(copy, severity, stackTrace: stackTrace); +} + +void debug( + String message, [ + Map payload = const {}, + StackTrace? stackTrace, +]) => write(LogSeverity.debug, message, payload, stackTrace); + +void info( + String message, { + Map payload = const {}, + StackTrace? stackTrace, +}) => write(LogSeverity.info, message, payload, stackTrace); + +void warning( + String message, [ + Map payload = const {}, + StackTrace? stackTrace, +]) => write(LogSeverity.warning, message, payload, stackTrace); + +void error( + String message, [ + Map payload = const {}, + StackTrace? stackTrace, +]) => write(LogSeverity.error, message, payload, stackTrace); diff --git a/lib/src/server.dart b/lib/src/server.dart index 1f887ad..796cc1e 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -21,6 +21,7 @@ import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart'; import 'package:stack_trace/stack_trace.dart' show Trace; +import '../logger.dart' as logger; import 'common/cloud_run_id.dart'; import 'common/environment.dart'; import 'common/on_init.dart'; diff --git a/lib/src/tasks/tasks_namespace.dart b/lib/src/tasks/tasks_namespace.dart index 9dc86cb..352108f 100644 --- a/lib/src/tasks/tasks_namespace.dart +++ b/lib/src/tasks/tasks_namespace.dart @@ -21,6 +21,7 @@ import 'package:shelf/shelf.dart'; import 'package:stack_trace/stack_trace.dart' show Trace; +import '../../logger.dart' as logger; import '../firebase.dart'; import '../logger/logger.dart'; import 'options.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 252d0d3..c1d56fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,3 +61,9 @@ dev_dependencies: mocktail: ^1.0.4 test: ^1.29.0 yaml: ^3.1.3 + +dependency_overrides: + google_cloud_logging: + path: /Users/bquinlan/dart/google-cloud-dart/pkgs/google_cloud_logging/ + google_cloud_shelf: + path: /Users/bquinlan/dart/google-cloud-dart/pkgs/google_cloud_shelf/ \ No newline at end of file From ba99206612fe94aea5d625cc206361a027b4c285 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 29 May 2026 14:05:57 -0700 Subject: [PATCH 6/8] fixes --- example/https/bin/server.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/https/bin/server.dart b/example/https/bin/server.dart index 3afc1de..81ed241 100644 --- a/example/https/bin/server.dart +++ b/example/https/bin/server.dart @@ -13,7 +13,6 @@ // limitations under the License. import 'package:firebase_functions/firebase_functions.dart'; -import 'package:firebase_functions/logger.dart'; // Define parameters - these are read from environment variables at runtime // and can be configured at deploy time via .env files or CLI prompts. @@ -123,7 +122,6 @@ void main(List args) async { // ignore: non_const_argument_for_const_parameter options: HttpsOptions(minInstances: DeployOption.param(minInstances)), (request) async { - error('This is a request', {'something': 'else'}); return Response.ok(welcomeMessage.value()); }, ); From 8ea349030b4f43c9d4705171f9cac41e959e99c2 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 29 May 2026 14:06:03 -0700 Subject: [PATCH 7/8] fix --- lib/src/https/callable.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/https/callable.dart b/lib/src/https/callable.dart index 195538b..c69d683 100644 --- a/lib/src/https/callable.dart +++ b/lib/src/https/callable.dart @@ -20,7 +20,6 @@ import 'dart:typed_data'; import 'package:shelf/shelf.dart'; import '../../logger.dart' as logger; -import '../logger/logger.dart'; import 'error.dart'; /// JSON decoder function type. From 84c256ffaea50c030e6b4d392fcc583328ff1fae Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 29 May 2026 14:16:04 -0700 Subject: [PATCH 8/8] fixes --- lib/src/server.dart | 1 - lib/src/tasks/tasks_namespace.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/src/server.dart b/lib/src/server.dart index 796cc1e..3faae57 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -26,7 +26,6 @@ import 'common/cloud_run_id.dart'; import 'common/environment.dart'; import 'common/on_init.dart'; import 'firebase.dart'; -import 'logger/logger.dart'; /// Callback type for the user's function registration code. typedef FunctionsRunner = FutureOr Function(Firebase firebase); diff --git a/lib/src/tasks/tasks_namespace.dart b/lib/src/tasks/tasks_namespace.dart index 352108f..76411c0 100644 --- a/lib/src/tasks/tasks_namespace.dart +++ b/lib/src/tasks/tasks_namespace.dart @@ -23,7 +23,6 @@ import 'package:stack_trace/stack_trace.dart' show Trace; import '../../logger.dart' as logger; import '../firebase.dart'; -import '../logger/logger.dart'; import 'options.dart'; import 'task_request.dart';