Skip to content

Commit

Permalink
feat(cloud_http): Add utils package (#37)
Browse files Browse the repository at this point in the history
Adds a package with helpers for running Dart server apps in a cloud environments. Includes tracing and logging helpers.
  • Loading branch information
dnys1 committed Sep 23, 2024
1 parent 8d6a43a commit dc0660c
Show file tree
Hide file tree
Showing 15 changed files with 1,268 additions and 1 deletion.
52 changes: 52 additions & 0 deletions .github/workflows/cloud_http.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: cloud_http
on:
pull_request:
paths:
- ".github/workflows/cloud_http.yaml"
- "packages/cloud_http/**"

# Prevent duplicate runs due to Graphite
# https://graphite.dev/docs/troubleshooting#why-are-my-actions-running-twice
concurrency:
group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || ''}}
cancel-in-progress: true

jobs:
check:
strategy:
fail-fast: true
matrix:
sdk:
- stable
- "3.3"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Git Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7
with:
submodules: recursive
- name: Setup Dart
uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # 1.6.5
with:
sdk: ${{ matrix.sdk }}
- name: Create override
working-directory: packages/cloud_http
run: |
cat <<EOF > pubspec_overrides.yaml
dependency_overrides:
http_sfv:
path: ../http_sfv
EOF
- name: Get Packages
working-directory: packages/cloud_http
run: dart pub get
- name: Analyze
working-directory: packages/cloud_http
run: dart analyze
- name: Format
working-directory: packages/cloud_http
run: dart format --set-exit-if-changed .
- name: Test
working-directory: packages/cloud_http
run: dart test
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.DS_Store
.DS_Store
pubspec_overrides.yaml
7 changes: 7 additions & 0 deletions packages/cloud_http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
3 changes: 3 additions & 0 deletions packages/cloud_http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial version.
3 changes: 3 additions & 0 deletions packages/cloud_http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# cloud_http

Utilities for running Dart server applications in cloud environments.
1 change: 1 addition & 0 deletions packages/cloud_http/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
5 changes: 5 additions & 0 deletions packages/cloud_http/lib/cloud_http.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
library;

export 'src/tracing/trace_context.dart';
export 'src/tracing/trace_parent.dart';
export 'src/tracing/trace_state.dart';
66 changes: 66 additions & 0 deletions packages/cloud_http/lib/src/tracing/middleware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:math';

import 'package:cloud_http/cloud_http.dart';
import 'package:convert/convert.dart';
import 'package:shelf/shelf.dart' as shelf;

/// A trace context [shelf.Middleware] which conforms to the W3C Trace Context
/// specification.
///
/// See: https://w3c.github.io/trace-context/
shelf.Middleware tracingMiddleware({
Random? random,
}) {
random ??= Random.secure();
return (shelf.Handler innerHandler) {
return (shelf.Request request) {
var traceContext = TraceContext.fromHeaders(request.headers);
var traceparent = traceContext.traceparent;

if (traceparent == null) {
// A vendor receiving a request without a traceparent header SHOULD
// generate traceparent headers for outbound requests, effectively
// starting a new trace.
traceparent = Traceparent.create(
traceId: hex.encode(
List<int>.generate(16, (_) => random!.nextInt(256)),
),
parentId: hex.encode(
List<int>.generate(8, (_) => random!.nextInt(256)),
),
sampled: true,
random: true,
);
} else {
// https://www.w3.org/TR/trace-context-2/#a-traceparent-is-received
//
// The vendor MUST modify the traceparent header:
// - Update parent-id: The value of property parent-id MUST be set to a
// value representing the ID of the current operation.
// - Update sampled: The value of sampled reflects the caller's
// recording behavior. The value of the sampled flag of trace-flags
// MAY be set to 1 if the trace data is likely to be recorded or to 0
// otherwise. Setting the flag is no guarantee that the trace will be
// recorded but increases the likeliness of end-to-end recorded traces.
traceparent = traceparent.copyWith(
parentId: hex.encode(
List<int>.generate(8, (_) => random!.nextInt(256)),
),
sampled: true,
);
}

traceContext = TraceContext(
traceparent: traceparent,
tracestate: traceContext.tracestate,
);

return innerHandler(
request.change(headers: {
...request.headers,
...traceContext.toHeaders(),
}),
);
};
};
}
76 changes: 76 additions & 0 deletions packages/cloud_http/lib/src/tracing/trace_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:cloud_http/src/tracing/trace_parent.dart';
import 'package:cloud_http/src/tracing/trace_state.dart';

/// The trace context for a request, as defined in the W3C Trace Context
/// [specification](https://www.w3.org/TR/trace-context-2).
final class TraceContext {
const TraceContext({
required Traceparent this.traceparent,
this.tracestate,
});

const TraceContext._({
this.traceparent,
this.tracestate,
});

/// Parses the `traceparent` and `tracestate` headers from a request's
/// [headers].
///
/// If the `traceparent` header is missing or invalid, the returned context
/// will have a
///
/// If the `tracestate` header is missing or invalid, the returned context
/// will have a `null` [tracestate].
///
/// Assumes that [headers] is a case-insensitive [Map].
factory TraceContext.fromHeaders(Map<String, Object>? headers) {
final traceparentHeader = switch (headers?['traceparent']) {
null => null,
final String traceparent => traceparent,
final List<String> traceparent => traceparent.singleOrNull,
final invalid => throw FormatException(
'Invalid traceparent header: $invalid. '
'Expected String or List<String>, got ${invalid.runtimeType}.',
),
};
final traceparent = traceparentHeader != null
? Traceparent.tryParse(traceparentHeader)
: null;
// If the vendor failed to parse traceparent, it MUST NOT attempt to parse
// tracestate. Note that the opposite is not true: failure to parse
// tracestate MUST NOT affect the parsing of traceparent.
Tracestate? tracestate;
if (traceparent != null) {
final tracestateHeader = switch (headers?['tracestate']) {
null => null,
final String tracestate => tracestate,
// Multiple tracestate header fields MUST be handled as specified by
// RFC9110 Section 5.3 Field Order.
final List<String> tracestate => tracestate.join(', '),
final invalid => throw FormatException(
'Invalid tracestate header: $invalid. '
'Expected String or List<String>, got ${invalid.runtimeType}.',
),
};
tracestate = tracestateHeader != null
? Tracestate.tryParse(tracestateHeader)
: null;
}
return TraceContext._(
traceparent: traceparent,
tracestate: tracestate,
);
}

final Traceparent? traceparent;
final Tracestate? tracestate;

Map<String, String> toHeaders() => {
// In order to increase interoperability across multiple protocols and
// encourage successful integration, tracing systems SHOULD encode the
// header name as ASCII lowercase.
if (traceparent != null) 'traceparent': traceparent.toString(),
if (tracestate != null) 'tracestate': tracestate.toString(),
};
}
Loading

0 comments on commit dc0660c

Please sign in to comment.