diff --git a/pkgs/google_cloud_logging/lib/src/logger.dart b/pkgs/google_cloud_logging/lib/src/logger.dart index 2dac8dcf..364277e4 100644 --- a/pkgs/google_cloud_logging/lib/src/logger.dart +++ b/pkgs/google_cloud_logging/lib/src/logger.dart @@ -12,10 +12,53 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; + import 'package:google_cloud_logging_type/logging_type.dart' as logging_type; import 'structured_logging.dart' show createStructuredLog, formatStackTrace; +/// The `payload` key used to correlate log entries with Cloud Trace. +/// +/// See https://docs.cloud.google.com/logging/docs/agent/logging/configuration#special-fields +const _logTraceKey = 'logging.googleapis.com/trace'; + +/// The `payload` key used to correlate log entries with a specific span within +/// a Cloud Trace. +/// +/// See https://docs.cloud.google.com/logging/docs/agent/logging/configuration#special-fields +const _logSpanIdKey = 'logging.googleapis.com/spanId'; + +/// The `payload` key used to indicate whether a trace is sampled. +/// +/// See https://docs.cloud.google.com/logging/docs/agent/logging/configuration#special-fields +const _logTraceSampledKey = 'logging.googleapis.com/trace_sampled'; + +// See: +// https://www.w3.org/TR/trace-context/#traceparent-header-field-values +const _version = r'^(?!ff)(?[0-9a-f]{2})'; +const _trace = r'(?!0{32})(?[0-9a-f]{32})'; +const _parent = r'(?!0{16})(?[0-9a-f]{16})'; +const _flags = r'(?[0-9a-f]{2})'; +final _traceParentRegex = RegExp('$_version-$_trace-$_parent-$_flags'); + +/// Parsers a `'tracecontext'` header. +/// +/// See https://www.w3.org/TR/trace-context/ +({String traceId, String spanId, bool traceSampled})? _parseTraceparent( + String traceparent, +) { + final match = _traceParentRegex.firstMatch(traceparent); + if (match == null) return null; + + final flags = int.parse(match.namedGroup('flags')!, radix: 16); + return ( + traceId: match.namedGroup('trace')!, + spanId: match.namedGroup('parent')!, + traceSampled: flags & 1 == 1, + ); +} + /// Base class for logging. /// /// Extend this class to create your own logger or use the @@ -188,10 +231,24 @@ final class _StructuredLogger extends CloudLogger { Map? payload, StackTrace? stackTrace, }) { + var p = payload; + final traceparent = Zone.current['traceparent']; + if (traceparent is String) { + final traceData = _parseTraceparent(traceparent); + if (traceData != null) { + p = { + ...?payload, + _logTraceKey: traceData.traceId, + _logSpanIdKey: traceData.spanId, + if (traceData.traceSampled) _logTraceSampledKey: true, + }; + } + } + final logEntry = createStructuredLog( message, severity, - payload: payload, + payload: p, stackTrace: stackTrace, ); print(logEntry); diff --git a/pkgs/google_cloud_logging/test/logger_test.dart b/pkgs/google_cloud_logging/test/logger_test.dart index 829b584c..934d59c6 100644 --- a/pkgs/google_cloud_logging/test/logger_test.dart +++ b/pkgs/google_cloud_logging/test/logger_test.dart @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; import 'dart:convert'; import 'package:google_cloud_logging/google_cloud_logging.dart'; @@ -128,5 +129,158 @@ Payload: {foo: bar} ), ); }); + + group('traceparent zone variable', () { + test('sampled true', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + { + 'message': 'hello', + 'severity': 'INFO', + 'logging.googleapis.com/trace': + '4bf92f3577b34da6a3ce929d0e0e4736', + 'logging.googleapis.com/spanId': '00f067aa0ba902b7', + 'logging.googleapis.com/trace_sampled': true, + }, + ), + ), + ), + zoneValues: { + 'traceparent': + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + }, + ); + }); + + test('sampled false', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + { + 'message': 'hello', + 'severity': 'INFO', + 'logging.googleapis.com/trace': + '4bf92f3577b34da6a3ce929d0e0e4736', + 'logging.googleapis.com/spanId': '00f067aa0ba902b7', + }, + ), + ), + ), + zoneValues: { + 'traceparent': + '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + }, + ); + }); + + test('valid higher version traceparent with extra fields', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + { + 'message': 'hello', + 'severity': 'INFO', + 'logging.googleapis.com/trace': + '4bf92f3577b34da6a3ce929d0e0e4736', + 'logging.googleapis.com/spanId': '00f067aa0ba902b7', + 'logging.googleapis.com/trace_sampled': true, + }, + ), + ), + ), + zoneValues: { + 'traceparent': + '01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' + '-extra-fields', + }, + ); + }); + + test('truncated', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + {'message': 'hello', 'severity': 'INFO'}, + ), + ), + ), + zoneValues: {'traceparent': '00-123'}, + ); + }); + + test('version ff', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + {'message': 'hello', 'severity': 'INFO'}, + ), + ), + ), + zoneValues: { + 'traceparent': + 'ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + }, + ); + }); + + test('all zeroes trace-id', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + {'message': 'hello', 'severity': 'INFO'}, + ), + ), + ), + zoneValues: { + 'traceparent': + '00-00000000000000000000000000000000-00f067aa0ba902b7-01', + }, + ); + }); + + test('all zeroes parent-id', () { + runZoned( + () => expect( + () => logger.info('hello'), + prints( + isA().having( + (s) => jsonDecode(s) as Map, + 'parsed json', + {'message': 'hello', 'severity': 'INFO'}, + ), + ), + ), + zoneValues: { + 'traceparent': + '00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01', + }, + ); + }); + }); }); } diff --git a/pkgs/google_cloud_shelf/lib/src/http_logging.dart b/pkgs/google_cloud_shelf/lib/src/http_logging.dart index 638c4a01..8df6a2c6 100644 --- a/pkgs/google_cloud_shelf/lib/src/http_logging.dart +++ b/pkgs/google_cloud_shelf/lib/src/http_logging.dart @@ -306,6 +306,7 @@ Middleware cloudLoggingMiddleware(String projectId) { Zone.current .fork( zoneValues: { + 'traceparent': request.headers['traceparent'], _loggerKey: _CloudLogger( zone: currentZone, traceContext: traceContext,