Skip to content

Added structured log JSON masking with generated metadata - #734

Open
GoodforGod wants to merge 3 commits into
masterfrom
feature/logging-argument-masking
Open

Added structured log JSON masking with generated metadata#734
GoodforGod wants to merge 3 commits into
masterfrom
feature/logging-argument-masking

Conversation

@GoodforGod

@GoodforGod GoodforGod commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

EN

Added automatic JSON structured logging for @Json arguments and results, plus masked JSON logging for values marked with @Mask. Masking metadata is generated for Java AP and KSP models annotated with @Mask, including nested model discovery through generic containers.


RU

Добавлена автоматическая запись аргументов и результатов @Log в JSON через @Json, а также маскирование JSON-вывода через @Mask. Метаданные маскирования генерируются для Java AP и KSP моделей, помеченных @Mask, включая вложенные модели внутри generic-контейнеров.


  • Added @Mask with FULL, KEEP_FIRST, and KEEP_LAST masking modes.
  • Added default @Json and @Tag(Mask.class) StructuredArgumentMapper providers for JSON and masked JSON logging.
  • Added generated MaskingMetadata components for Java AP and KSP, with nested metadata collection for records/data classes and container value types.
  • Improved tag-only mapper lookup so @Json can resolve tagged StructuredArgumentMapper instances without an explicit mapper class.

Masked Structured Logging

Added @Mask support for method parameters and @Log.out results when the logged value has generated JSON writer and masking metadata.

@Mask
@Json
public record User(String name, @Mask String token) {}

@Log.out
@Mask
public User test() {
    return new User("user", "secret");
}

Generated metadata is registered as a component and used by the masked structured mapper:

@Component
public final class $User_MaskingMetadata implements MaskingMetadata<User> {
    private final Map<Class<?>, MaskingClassMeta> metadata;
}

Added automatic JSON structured argument mapping and masked JSON logging for annotated arguments and results. Mask metadata is generated for Java AP and KSP models annotated with `@Mask`, including nested record/data-class structures and generic container traversal.

- Added `@Mask` rules with FULL, KEEP_FIRST, and KEEP_LAST modes.
- Added default `@Json` and `@Tag(Mask.class)` StructuredArgumentMapper providers.
- Added runtime masking support through generated MaskingMetadata and masking JsonGenerator delegation.
- Improved tag-only mapper lookup so @JSON can resolve tagged StructuredArgumentMapper instances without an explicit mapper class.
@GoodforGod GoodforGod added this to the v2.0.0 milestone Jul 18, 2026
@GoodforGod
GoodforGod requested a review from Squiry July 18, 2026 10:38
@GoodforGod GoodforGod added the new feature New feature request label Jul 18, 2026
@GoodforGod
GoodforGod marked this pull request as ready for review July 18, 2026 10:38
@GoodforGod GoodforGod added the module: logging Related to Logging module label Jul 18, 2026
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

Test Results

732 tests   723 ✅  25m 52s ⏱️
 99 suites    9 💤
 99 files      0 ❌

Results for commit 9a98f12.

♻️ This comment has been updated with latest results.

@github-actions

Copy link
Copy Markdown

Dependency Update Report

Update level: patch

Found 3 dependency updates.

gradle/libs.versions.toml

  • s3client-aws (software.amazon.awssdk:s3, inline:248): 2.47.5 -> 2.47.6
  • zeebe: 8.9.12 -> 8.9.13
  • kotlin-stdlib: 2.4.0 -> 2.4.10

Added declarative masking rules for logged values, including generated default MaskingRules components and custom @Mapping-selected rules for @Mask parameters and results.

- Added MaskingStrategy-based masking with built-in full, keep-first, and keep-last strategies.
- Added recursive path-based MaskingRules generation for Java AP and KSP, including nested collections and map value wildcards.
- Improved masked logging so @Mask writes masked JSON as a string by default, while @Mask @JSON keeps structured JSON output.
- Added support for custom MaskingRules implementations selected through @mapping on logged arguments and result values.
Added declarative masking rules for logged values, including generated default MaskingRules components and custom @Mapping-selected rules for @Mask parameters and results.

- Added MaskingStrategy-based masking with built-in full, keep-first, and keep-last strategies.
- Added recursive path-based MaskingRules generation for Java AP and KSP, including nested collections and map value wildcards.
- Improved masked logging so @Mask writes masked JSON as a string by default, while @Mask @JSON keeps structured JSON output.
- Added support for custom MaskingRules implementations selected through @mapping on logged arguments and result values.
@GoodforGod

Copy link
Copy Markdown
Contributor Author

Added masked log arguments with custom rules and JSON output modes

EN

Added declarative masking for logged arguments and result values with generated default MaskingRules<T> and custom @Mapping overrides. Plain @Mask now writes masked JSON as a string value, while @Mask @Json keeps structured JSON output.


RU

Добавлено декларативное маскирование аргументов и результатов логгирования через сгенерированные MaskingRules<T> и пользовательские правила через @Mapping. Обычный @Mask теперь пишет замаскированный JSON как строковое значение, а @Mask @Json сохраняет структурированный JSON в логе.


  • Added MaskingRules<T> generation for Java AP and KSP as @Module factory methods marked with @DefaultComponent.
  • Added MaskingStrategy support with default MaskingFull, MaskingKeepFirst, and MaskingKeepLast strategies.
  • Added recursive path-based masking rules with collection support and wildcard map-value paths.
  • Added custom MaskingRules selection through @Mapping on @Mask parameters and result values.
  • Improved masked mapper behavior so untagged @Mask logs masked JSON as a string, while @Mask @Json logs structured masked JSON.

Design

This change introduces masking as a separate logging-time layer on top of existing JSON serialization. The JSON writer remains responsible for object traversal and field emission, while masking is applied by wrapping the target JsonGenerator with MaskingJsonGenerator.

The main public concepts are:

  • @Mask marks a type, parameter, result, or field as participating in masking.
  • MaskingStrategy defines how a matched non-null value is replaced before it is written to the log.
  • MaskingRules<T> defines which JSON field names or root-relative JSON paths should be masked for a logged root type.
  • MaskedStructuredArgumentMapper<T> combines JsonWriter<T> and MaskingRules<T> and applies the rules while logging.

For a type annotated with @Mask, the annotation processors generate a module next to the model type:

@Module
@Generated(...)
public interface $User_MaskingRulesModule {
    @DefaultComponent
    default MaskingRules<User> userMaskingRules(MaskingFull strategy0) {
        return MaskingRules.builder(User.class)
            .mask("credentials.secret", strategy0)
            .build();
    }
}

The generated module is enough for graph discovery: there is no generated MaskingRules subclass and no Kora extension for these rules. The generated factory is marked as @DefaultComponent, so applications can override it with their own MaskingRules<User> component when needed.

Rules are path-based rather than type-based. This matters when the same nested type appears in different places: masking is generated relative to the logged root object, so user.password and manager.password can be treated as different paths. A single-segment rule such as password still works as a global field-name rule and masks every field with that JSON name.

Collections and arrays do not add path segments. Maps add a wildcard segment for values, so a structure like Map<String, List<Credentials>> credentialsById can generate a rule like:

MaskingRules.builder(User.class)
    .mask("credentialsById.*.secret", strategy)
    .build();

Map keys are not masked as values. The wildcard is used to match the dynamic JSON property name introduced by a map key and then continue matching the value path.

Masking strategies are regular components. The built-in defaults are provided by LoggingModule:

@DefaultComponent
default MaskingFull maskingStrategyFull() {
    return new MaskingFull();
}

The same pattern exists for MaskingKeepFirst and MaskingKeepLast. Users can define their own MaskingStrategy implementation and reference it from @Mask(CustomStrategy.class).

For complete control, users can provide a custom MaskingRules<T> implementation and select it at the logging site with @Mapping:

public final class CustomRules extends MaskingRules<User> {
    public CustomRules() {
        super(User.class, Map.of("token", value -> "masked"));
    }
}

@Log.in
public void test(@Mask @Mapping(CustomRules.class) User user) {}

The AOP generator now understands this mapping separately from StructuredArgumentMapper mappings. When @Mapping(CustomRules.class) points to MaskingRules<T>, the generated proxy asks the graph for the selected JsonWriter<T> and CustomRules, then creates the masked mapper from those two dependencies.

Output mode depends on annotations:

@Log.in
public void plain(@Mask User user) {}

@Log.in
public void structured(@Mask @Json User user) {}

@Mask writes the masked JSON payload as a string field in the final structured log argument:

{"user":"{\"token\":\"***\"}"}

@Mask @Json writes the masked payload as structured JSON:

{"user":{"token":"***"}}

This keeps the default untagged masking behavior conservative for logs that should not expand arbitrary complex objects into structured fields, while still allowing explicitly JSON-tagged arguments and results to remain structured.

The same rules apply to @Log.out / @Log.result values: @Mask result values are masked and written as a string JSON payload by default, and @Mask @Json result values stay structured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module: logging Related to Logging module new feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant