DcsvIo.D2.Logging
0.1.2
dotnet add package DcsvIo.D2.Logging --version 0.1.2
NuGet\Install-Package DcsvIo.D2.Logging -Version 0.1.2
<PackageReference Include="DcsvIo.D2.Logging" Version="0.1.2" />
<PackageVersion Include="DcsvIo.D2.Logging" Version="0.1.2" />
<PackageReference Include="DcsvIo.D2.Logging" />
paket add DcsvIo.D2.Logging --version 0.1.2
#r "nuget: DcsvIo.D2.Logging, 0.1.2"
#:package DcsvIo.D2.Logging@0.1.2
#addin nuget:?package=DcsvIo.D2.Logging&version=0.1.2
#tool nuget:?package=DcsvIo.D2.Logging&version=0.1.2
DcsvIo.D2.Logging
Serilog configuration + the [RedactData] enforcement layer + an ASP.NET Core request-logging middleware. Foundation lib that other shared libs and per-service composition roots call to wire the Serilog pipeline + the [RedactData] destructuring policy + the request-logging middleware (services call AddD2Logging instead of duplicating ~30 lines of LoggerConfiguration boilerplate per service). Also usable standalone by any host that wants the same Serilog setup without an aggregator.
Install
dotnet add package DcsvIo.D2.Logging
The lib does NOT own:
- OpenTelemetry SDK setup — out of scope. Logs reach OTLP collectors via the MEL pipeline (
writeToProviders: trueroutes Serilog output to other registeredILoggerProviders); the OTLP log-exporter wiring is owned by separate observability infrastructure. [LoggerMessage]source-generated delegates — this lib bootstraps the log pipeline itself; logging inside its own bootstrap path would be circular.- Activity / span enrichment (e.g.
Serilog.Enrichers.Span) — out of scope; observability infrastructure owns activity / span emission separately.
Public API surface
Composition
services.AddD2Logging(configuration, opts =>
{
opts.ServiceName = "edge"; // optional — defaults from OTEL_SERVICE_NAME → IHostEnvironment.ApplicationName
opts.Environment = "Production"; // optional — defaults from IHostEnvironment.EnvironmentName
opts.MinimumLevel = LogEventLevel.Information; // optional — default Information
});
// later, inside the middleware pipeline:
app.UseRouting();
app.UseD2RequestLogging(); // optional configure callback
app.UseEndpoints(...);
AddD2Logging(IConfiguration, Action<D2LoggingOptions>?) builds the Serilog LoggerConfiguration, sets Log.Logger, and wires the MEL bridge (AddSerilog(preserveStaticLogger: true, writeToProviders: true)). Validates options at the first IOptions<D2LoggingOptions>.Value resolution (typically host-startup composition) via ValidateOnStart() — fail-fast on invalid config.
UseD2RequestLogging(Action<RequestLoggingOptions>?) wraps Serilog's UseSerilogRequestLogging with the D² defaults: per-source minimum-level overrides, infrastructure-path suppression (logged at Verbose so the default minimum-level gate filters them out), conservative diagnostic-context enrichment, and projection of the spec-driven IRequestContext LOG-OK fields onto the request-completion event.
Configuration — D2LoggingOptions
| Property | Type | Default | Notes |
|---|---|---|---|
ServiceName |
string? |
OTEL_SERVICE_NAME config → IHostEnvironment.ApplicationName |
Emitted on every log line via the service_name structured property. Validated non-empty / non-whitespace at startup. |
Environment |
string? |
IHostEnvironment.EnvironmentName |
Emitted on every log line via the environment structured property. Validated non-empty / non-whitespace at startup. |
MinimumLevel |
LogEventLevel |
Information |
Default minimum log level. Per-source overrides applied independently — see "Per-source overrides" below. |
InfrastructurePaths |
IReadOnlyList<string> |
["/health", "/alive", "/metrics", "/.well-known"] |
Path prefixes whose request-completion events emit at Verbose instead of Information, so the default level gate filters them out. Validated non-empty (collection) and per-entry non-empty / non-whitespace at startup. |
Constants
D2LoggingConstants.OTEL_SERVICE_NAME_CONFIG_KEY — the "OTEL_SERVICE_NAME" configuration key consulted for the ServiceName default. Matches the OpenTelemetry-canonical convention so log + trace + metric service names stay aligned across the log + trace + metric pipelines without this lib taking an OpenTelemetry SDK dependency itself.
Per-source overrides
AddD2Logging applies these per-source minimum-level overrides on top of the configured MinimumLevel:
| Source | Override |
|---|---|
Microsoft.AspNetCore |
Warning |
Microsoft.Extensions.Http |
Warning |
System.Net.Http |
Warning |
D2 |
Debug |
Suppresses the framework / HTTP-client noise that Information-level produces by default while keeping D² code's diagnostic logs visible.
Enrichers
| Enricher | What it adds |
|---|---|
FromLogContext |
Per-scope properties pushed via LogContext.PushProperty propagate to nested log calls. |
WithMachineName |
MachineName structured property. |
WithProperty("service_name", …) |
The configured ServiceName. |
WithProperty("environment", …) |
The configured Environment. |
Output sink
Console with CompactJsonFormatter (terse JSON to stdout). When other ILoggerProviders are registered against builder.Logging (e.g. an OTLP log-exporter from observability infrastructure), Serilog routes events through both sinks via writeToProviders: true.
Network/IP enrichment design
The middleware does NOT log the request's connection-remote IP address (HttpContext.Connection.RemoteIpAddress). Two reasons:
- At internal services, that address is the upstream Edge instance's IP, not the original client's. Operators reading logs would interpret it as the user's IP, which it isn't — a data-shape footgun.
- At Edge, the resolved client IP is PII. Logging it as a structured-context field would bypass the
[RedactData]destructuring policy entirely (diagnostic-contextSetwrites aScalarValuedirectly).
Geo / network-privacy / ASN / identity / impersonation / tracing fields are projected from the spec-driven IRequestContext onto the request-completion log line via D2RequestContextEnricher. The enricher is always-on; it gracefully no-ops when IRequestContext isn't in DI scope. Each per-field Set is gated by a null check; null fields are not emitted at all (vs an emitted-null structured property). Collection fields (Audience, ActorChain, Scopes) gate on Truthy() so empty collections don't pollute logs with empty arrays on every request.
Why log identity at the request-context axis
Internal logs are read by operators who already have tenant access to query org / user metadata via authenticated APIs; emitting identity into the request-completion log line is operationally invaluable for "show me Alice's requests across this trace" debugging without any incremental disclosure beyond what those same operators can already query directly. The trade-off is intentional: every log line that fires inside the user's own access boundary already carries identity through trace + correlation IDs anyway; the enricher just makes the linkage one hop shorter.
Precedence note — TraceId, RequestId, RequestPath
The three Tracing-axis fields collide with values that the request-logging middleware emits independently. The override behavior differs per field because of how each upstream value reaches the log event.
TraceId— enricher WINS when populated. The middleware setsTraceIdviaIDiagnosticContext.Set("TraceId", httpContext.TraceIdentifier)early in theEnrichDiagnosticContextcallback. The enricher runs LAST in that callback and writes via the sameIDiagnosticContextmechanism — last-writer-wins on the same diagnostic-context dictionary. WhenIRequestContext.TraceIdis populated (carrying the W3C distributed-trace id), the enricher overrides the localHttpContext.TraceIdentifiervalue. When unpopulated, the middleware's value survives. Operators reading logs should treatTraceIdas the W3C value whenIRequestContextis wired and asHttpContext.TraceIdentifierotherwise.RequestId— Serilog WINS on the HTTP path. Serilog 9.x'sRequestLoggingMiddlewareemitsRequestIdvia its ownForContextbinding before theEnrichDiagnosticContextcallback runs. Once Serilog has set the property on theLogEvent, subsequentIDiagnosticContext.Set("RequestId", ...)calls are silently dropped by Serilog'sAddPropertyIfAbsentsemantics. The enricher's emission ofIRequestContext.RequestIdis included for spec-contract completeness — on transports where Serilog does NOT pre-bindRequestId(custom diag-ctx integrations on non-HTTP transports), the enricher's value will appear.RequestPath— Serilog WINS on the HTTP path. Same rationale asRequestId. Serilog's request-completion message template bindsRequestPathfromHttpContext.Request.PathviaForContextbefore the enricher runs; the enricher's emission is silently dropped on HTTP. On AMQP / messaging transports without the same template binding, the enricher's value would appear.
The two HTTP-path limitations (RequestId and RequestPath) reflect Serilog's request-logging middleware design, not a bug in the enricher. Carrying the same emission contract across all transports (HTTP + AMQP + future) is the spec-driven discipline; the limitation is documented here so operators know RequestPath in HTTP logs is always the local path and RequestId is always the local TraceIdentifier.
LOG-OK fields (emitted when populated)
42 fields organized into 11 clusters. Every field is null-gated (collections gated on Truthy()); unpopulated fields don't reach the log line.
Tracing (3 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 1 | TraceId |
string? |
W3C distributed-trace id propagated across hops. Overrides the middleware's local TraceId when populated (see precedence note above). |
| 2 | RequestId |
string? |
Per-request unique id (HttpContext.TraceIdentifier for HTTP; AMQP message-id for messaging). Not PII — synthetic identifier. HTTP-path limitation: Serilog 9.x emits RequestId independently; the enricher's emission is silently dropped on HTTP. See precedence note. |
| 3 | RequestPath |
string? |
Request path / AMQP routing key. Route templates are operational metadata, not PII. HTTP-path limitation: Serilog's message template pre-binds RequestPath; the enricher's emission is silently dropped on HTTP. See precedence note. |
Auth/Identity (8 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 4 | IsAuthenticated |
bool? |
Trinary auth state (null=pre-auth, false=anonymous, true=authenticated). Capability metadata; not PII. |
| 5 | Subject |
string? |
Raw JWT sub claim — opaque identifier (Guid for users; client_id for service identities). Not PII per OAuth/JWT vocabulary. |
| 6 | UserId |
Guid? |
Parsed sub as Guid for user tokens; null for service identities. Opaque audit identifier. |
| 7 | Username |
string? |
Login handle (lowercase, unique). Users CHOOSE their username at signup; the value is the user's deliberate public identifier within the platform. PII consideration: a user MAY choose an email-shaped or name-shaped username (e.g. alice.smith@example.com, alicemarsh). The platform does not constrain user choice; the cost of capturing username in operational logs (where it's invaluable for "show me Alice's requests" debugging) outweighs the marginal privacy delta from a user CHOOSING to use email-shaped identifiers. The Subject raw-sub and UserId Guid are the canonical identifiers; Username is the human-readable supplement. |
| 8 | RequestedByClientId |
string? |
OAuth client_id of the client that requested THIS specific token (RFC 8693 §4.3 / RFC 9068 §2.2). Service identifier; not PII. |
| 9 | ImmediateCallerClientId |
string? |
The service that immediately called this handler — outermost Service entry in the act chain. Service identifier; not PII. |
| 10 | OriginatingClientId |
string? |
The service that started the entire call chain — most-deeply-nested Service entry. Primary audit identifier for end-to-end traceability. Service identifier; not PII. |
| 11 | IsServiceIdentity |
bool? |
True when the token represents a service identity (no user). Capability metadata; not PII. |
Auth/Token+Trust (5 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 12 | Audience |
IReadOnlyList<string> |
JWT aud claim — service identifier(s) the token was minted for. Destructured as JSON array. Empty-collection gate suppresses [] emission. |
| 13 | SessionId |
Guid? |
User's own session id in d2-auth (session row). Opaque audit identifier; not PII. |
| 14 | TokenIssuedAt |
DateTimeOffset? |
JWT iat claim. Operational timestamp; not PII. |
| 15 | TokenExpiresAt |
DateTimeOffset? |
JWT exp claim. Operational timestamp; not PII. |
| 16 | ActorChain |
IReadOnlyList<ActorEntry> |
RFC 8693 actor chain, flattened. Destructured as JSON array of ActorEntry objects. Every member of ActorEntry is a subset of fields already approved as LOG-OK on the top-level axis (Subject, OrgId, OrgName, OrgType, OrgRole, SessionId, ImpersonationKind), so destructure is §3-safe. Empty-collection gate suppresses [] emission for end-user-direct tokens. |
Auth/Org (4 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 17 | OrgId |
Guid? |
Operating organization id. Opaque identifier; not PII. |
| 18 | OrgName |
string? |
Operating organization display name. Internal logs are read by operators who already have tenant access to query org metadata via authenticated APIs; there is no incremental disclosure from emitting org names into logs that those same operators authenticate to access. Same rationale applies to ImpersonatorOrgName (the agent's own org during impersonation). |
| 19 | OrgType |
OrgType? (enum) |
Org type (Admin / Support / Customer / ThirdParty / Affiliate). Capability metadata; not PII. |
| 20 | OrgRole |
Role? (enum) |
User's role in the org (Auditor / Agent / Officer / Owner). Capability metadata; not PII. |
Auth/Impersonation (8 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 21 | IsImpersonating |
bool? |
True when the act chain contains any Impersonation entry. Capability metadata; not PII. |
| 22 | ImpersonationKind |
ImpersonationKind? (enum) |
Consent (OTP-authorized) vs Force (silent, admin-only). Capability metadata. |
| 23 | ImpersonatedBy |
Guid? |
Agent's (impersonator's) user id. Opaque identifier; not PII. |
| 24 | ImpersonationSessionId |
Guid? |
Impersonation session id (separate from user's own SessionId). Opaque identifier; not PII. |
| 25 | ImpersonatorOrgId |
Guid? |
Agent's own org id. Opaque identifier; not PII. |
| 26 | ImpersonatorOrgName |
string? |
Agent's own org display name. Same rationale as OrgName above. |
| 27 | ImpersonatorOrgType |
OrgType? (enum) |
Agent's own org type. Capability metadata. |
| 28 | ImpersonatorOrgRole |
Role? (enum) |
Agent's own role in their own org. Capability metadata. |
Scopes (1 field)
| # | Field | Type | Reason |
|---|---|---|---|
| 29 | Scopes |
IReadOnlySet<string> |
OAuth scope set. Destructured as JSON array (preserves field-array semantics for "show me requests with scope X" queries in Loki / Elasticsearch). Empty-collection gate suppresses [] emission for unauthenticated requests. |
Trust/Risk (1 field)
| # | Field | Type | Reason |
|---|---|---|---|
| 30 | RiskScore |
int? (0-100) |
Edge composite request-risk score. Derived numeric; not PII (zero reverse path to fingerprint inputs). |
Fingerprints (2 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 31 | SessionFingerprint |
string? |
Mint-time-bound fingerprint hash (SHA-256-derived components). Opaque hash. |
| 32 | CurrentFingerprint |
string? |
Per-request recomputed fingerprint hash. Opaque hash. Comparison with SessionFingerprint feeds risk scoring. |
WhoIs/Geo (3 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 33 | WhoIsHashId |
string? |
Content-addressable hash of the full WhoIs record. Opaque hash; not PII. Operators with auth can dereference for the full record (including suppressed sub-country geo) when needed. |
| 34 | AdminLocationHashId |
string? |
Content-addressable hash of admin-location component (city + region + country + postal). Opaque hash; not PII. |
| 35 | CountryCode |
string? (ISO 3166-1 alpha-2) |
Country grain only — explicitly above the "city + postal beyond country" PII line. |
WhoIs/Network-Privacy (4 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 36 | IsVpn |
bool? |
Derived boolean; not PII. |
| 37 | IsProxy |
bool? |
Derived boolean; not PII. |
| 38 | IsTor |
bool? |
Derived boolean; not PII. High signal for security review. |
| 39 | IsHosting |
bool? |
Derived boolean; not PII. Hosting-source requests behave differently. |
WhoIs/ASN (3 fields)
| # | Field | Type | Reason |
|---|---|---|---|
| 40 | Asn |
int? |
Public AS number (published BGP data); not PII. Risk-scoring + abuse-investigation signal. |
| 41 | AsnName |
string? |
Public AS organization name; not PII. |
| 42 | AsnType |
string? |
Coarse classification (business / isp / hosting / mobile / education / government); not PII. |
NOT-LOGGED fields (suppressed at default)
8 fields. The first 5 are explicitly user-pinned PII; the last 3 (lat/long/geohash) are §3-driven (geographic precision = identification primitive — geohash IS lat/long, just hex-encoded). Sub-country geographic precision escalates only via WhoIsHashId lookups against the WhoIs store (operators with auth can dereference when a real investigation needs it).
| Field | §3 cite | Reason |
|---|---|---|
ClientIp |
§3 "What counts as PII" — IP addresses (v4 + v6) | Raw IP is PII per §3. |
City |
§3 "What counts as PII" — sub-country geographic precision | Locality grain beyond country is PII. |
Region |
§3 "What counts as PII" — sub-country geographic precision | Locality grain beyond country is PII. |
SubdivisionCode |
§3 "What counts as PII" — sub-country geographic precision | ISO 3166-2 subdivision is sub-country precision; PII. |
PostalCode |
§3 "What counts as PII" — postal code | Locality grain beyond country is PII. |
Latitude |
§3 "What counts as PII" — geographic (lat/long) | GPS-exact precision is named in §3 as a PII primitive. |
Longitude |
§3 "What counts as PII" — geographic (lat/long) | GPS-exact precision is named in §3 as a PII primitive. |
Geohash |
§3 "What counts as PII" — geographic (lat/long, hex-encoded) | Street-level precision; same §3 rationale as raw lat/long. |
Opting in to additional fields
Callers can chain into the existing RequestLoggingOptions.EnrichDiagnosticContext callback:
app.UseD2RequestLogging(opts =>
{
var existing = opts.EnrichDiagnosticContext;
opts.EnrichDiagnosticContext = (diag, http) =>
{
existing?.Invoke(diag, http);
diag.Set("MyCustomField", ResolveCustomField(http));
};
});
⚠️ Caller-added structured fields BYPASS the [RedactData] destructuring policy (the diagnostic-context path emits ScalarValues directly). Callers MUST own PII discipline for anything they add. If the data is PII, log a [RedactData]-decorated wrapper object using the destructuring capture mode ({@MyObj}) inside a LoggerScope instead.
Destructuring discipline
RedactDataDestructuringPolicy enforces the [RedactData] attribute defined in DcsvIo.D2.Utilities.Attributes. It runs on every @-captured object in every ILogger.Log* call across every service that wires AddD2Logging. Three rules to know:
- Capture mode matters. Serilog's
@-prefix invokes destructuring; the policy fires. The default{prop}capture (without@) calls.ToString()on the value and bypasses destructuring entirely. If you have[RedactData]on a record and writelogger.Information("user {User}", user), the record's defaultToString()includes property values verbatim — leaks the PII the attribute was supposed to redact. - Field-level
[RedactData]is silently ignored. The policy reflects overBindingFlags.Public | InstancePROPERTIES only — no fields. Use property syntax for redaction. - Computed properties leak through their inputs. A
public string DisplayName => $"{First} {Last}"is destructured even ifFirstcarries[RedactData]. Redact the computed property too if its inputs are PII.
Redaction modes
| Decoration | Effect |
|---|---|
[RedactData] on the type |
Entire value rendered as [REDACTED: {Reason}]. |
[RedactData] on a property |
Only that property is masked; siblings destructure normally. |
[RedactData(CustomReason = "...")] |
The custom string replaces the enum-name reason in the placeholder. |
Reason rendering uses the enum name (PersonalInformation, FinancialInformation, SecretInformation, VerboseContent, Other, Unspecified) unless a CustomReason overrides it.
Types (source map)
| File | Role |
|---|---|
DcsvIo.D2.Logging.csproj |
csproj — Microsoft.NET.Sdk.Web + OutputType=Library. package references to utilities/ + context/abstractions/. |
D2LoggingOptions.cs |
Sealed record — Options-pattern config. |
D2LoggingConstants.cs |
Public constants (OTEL_SERVICE_NAME_CONFIG_KEY). |
LoggingServiceCollectionExtensions.cs |
Public DI extension: AddD2Logging. |
WebApplicationLoggingExtensions.cs |
Public AspNetCore extension: UseD2RequestLogging. |
Destructuring/RedactDataDestructuringPolicy.cs |
Internal sealed IDestructuringPolicy impl. |
Destructuring/TypeRedactionInfo.cs |
Internal sealed record — per-type cache value. |
Internal/D2RequestContextEnricher.cs |
Internal static helper that projects IRequestContext LOG-OK fields onto the request-completion log line. |
Dependencies
| Package | Why |
|---|---|
Serilog.AspNetCore |
The umbrella package. Pulls Serilog, Serilog.Extensions.Logging, Serilog.Sinks.Console, Microsoft.Extensions.Logging.Abstractions transitively. UseSerilogRequestLogging lives here. |
Serilog.Formatting.Compact |
CompactJsonFormatter. Listed explicitly so transitive removal can't silently drop it. |
Serilog.Enrichers.Environment |
Enrich.WithMachineName(). |
JetBrains.Annotations |
[MustDisposeResource] annotations on disposable factory paths. |
| Package dependency | Why |
|---|---|
DcsvIo.D2.Utilities |
[RedactData] attribute + RedactReason enum + Falsey() / Truthy() extensions. |
DcsvIo.D2.Context.Abstractions |
IRequestContext interface (the strongly-typed enricher dep). |
DcsvIo.D2.AspNetCore |
Canonical InfrastructurePathMatcher consumed by UseD2RequestLogging's level callback to demote infrastructure-path request-completion logs to Verbose. Single source of truth shared with DcsvIo.D2.Telemetry's AspNetCore-instrumentation Filter. |
Edge cases / gotchas
Log.Loggeris a process-global static.AddD2LoggingSETS it. Tests that build multiple hosts in one process see the LATEST host's logger on the static. The lib's integration tests pin this behavior with[Collection("LogLoggerStaticState")]to serialize against any other test that touches the static.Microsoft.NET.Sdk.WebdefaultsOutputTypetoExe. This csproj overrides toLibraryexplicitly — same shape as other ASP.NET-library siblings.- No
Microsoft.Extensions.{DependencyInjection,Logging,Options}.AbstractionsPackageReferences. The framework reference (Microsoft.AspNetCore.App) ships them; explicit references would trigger NU1510 ("will not be pruned"). Versions remain pinned viaDirectory.Packages.props. InfrastructurePathMatcherlives inDcsvIo.D2.AspNetCore— single source of truth.UseD2RequestLogging's level callback consumes the public matcher;DcsvIo.D2.Telemetry's AspNetCore-instrumentationFilterconsumes the same one. The path set (/health,/alive,/metrics,/.well-known) stays aligned across the two consumers without per-lib duplication.- The integration-test contract pins the absent PII fields.
RequestContextEnricherIntegrationTestsenumerates everyIRequestContextNOT-LOGGED field (the 8 enumerated above:ClientIp,City,Region,SubdivisionCode,PostalCode,Latitude,Longitude,Geohash) and asserts NONE appear in the rendered JSON output. Adding a new PII field to the spec without a coverage update is a contract failure that surfaces at test time.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DcsvIo.D2.AspNetCore (>= 0.1.2)
- DcsvIo.D2.Context.Abstractions (>= 0.1.1)
- DcsvIo.D2.Utilities (>= 0.1.1)
- dotenv.net (>= 4.0.2)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.EntityFrameworkCore (>= 10.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.7)
- Microsoft.IdentityModel.Tokens (>= 8.16.0)
- NodaTime (>= 3.2.2)
- Npgsql (>= 10.0.2)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.1)
- Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime (>= 10.0.1)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Enrichers.Environment (>= 3.0.1)
- Serilog.Formatting.Compact (>= 3.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.