Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
dfccdb9
[Automated] Update the native jar versions
TharmiganK Sep 1, 2025
3fc78ff
Add initial implementation
TharmiganK Sep 1, 2025
87787b6
Refactor with perf improvements
TharmiganK Sep 2, 2025
8ca08e7
Refactor by adding a builder
TharmiganK Sep 2, 2025
1b60ee2
Fix checkstyle issues
TharmiganK Sep 2, 2025
abce20d
Merge remote-tracking branch 'origin/master' into sensitive-data
TharmiganK Sep 2, 2025
0180d53
Add sensitive data masking support for root logger
TharmiganK Sep 3, 2025
7840838
Add annotation caching support
TharmiganK Sep 3, 2025
376f57e
Merge remote-tracking branch 'origin/master' into sensitive-data
TharmiganK Sep 3, 2025
c44d90d
Merge branch 'master' into sensitive-data
TharmiganK Sep 8, 2025
77f026d
Merge branch 'master' into sensitive-data
TharmiganK Sep 8, 2025
3490499
Fix xml toString
TharmiganK Sep 17, 2025
e6e3aa4
Support undefined fields in the record to string
TharmiganK Sep 17, 2025
1ad62cf
Add tests for masked string function
TharmiganK Sep 17, 2025
49eac01
Merge branch 'master' into sensitive-data
TharmiganK Sep 17, 2025
2f2e3cb
Refactor class to reduce cognitive complexity
TharmiganK Sep 17, 2025
7dbb203
Add support for map value
TharmiganK Sep 17, 2025
78298b2
Fix issues with field names with special characters
TharmiganK Sep 19, 2025
f989b46
Merge branch 'master' into sensitive-data
TharmiganK Sep 19, 2025
1eefb16
Merge branch 'master' into sensitive-data
TharmiganK Sep 26, 2025
466d4b8
Add support to enable sensitive data masking via configuration
TharmiganK Oct 2, 2025
868fca5
Add tests for masked logging
TharmiganK Oct 2, 2025
e04e2a2
Enhance type processing to handle intersection and reference types
TharmiganK Oct 2, 2025
1ad022a
Add support for sensitive data masking in templates and value functions
TharmiganK Oct 2, 2025
b6cb527
Add tests for readonly types
TharmiganK Oct 2, 2025
de5cdb1
Add an integration test
TharmiganK Oct 2, 2025
60a4cf4
Update changelog
TharmiganK Oct 2, 2025
11fdebd
Update spec
TharmiganK Oct 2, 2025
1d3bff0
Merge remote-tracking branch 'origin/master' into sensitive-data
TharmiganK Oct 2, 2025
7e27003
Optimize Unicode escaping by using a pre-computed hex lookup table
TharmiganK Oct 3, 2025
1dca782
Add tests for masking structurally similar records and basic types
TharmiganK Oct 3, 2025
d19d781
Add test for masking special characters in strings
TharmiganK Oct 3, 2025
dbf5679
Merge branch 'master' into sensitive-data
TharmiganK Oct 3, 2025
6516ecd
Update spec to clarify masking behavior and type extraction
TharmiganK Oct 3, 2025
652f323
Add tests for masking empty arrays, tables, and records
TharmiganK Oct 3, 2025
a467533
Merge branch 'master' into sensitive-data
TharmiganK Oct 7, 2025
f93c481
Add documentation for sensitive data masking features
TharmiganK Oct 7, 2025
dfc98fa
Enhance sensitive data masking documentation and functionality
TharmiganK Oct 7, 2025
7a8b9aa
Refactor sensitive data masking strategy to use a dedicated maskStrin…
TharmiganK Oct 7, 2025
cc0cc5d
Refactor sensitive data annotation from @SensitiveData to @Sensitive
TharmiganK Oct 7, 2025
a4a6766
Deprecate processTemplate function and replace with evaluateTemplate …
TharmiganK Oct 7, 2025
a3f61e9
Merge branch 'master' into sensitive-data
TharmiganK Oct 8, 2025
37603fb
Address review suggestions
TharmiganK Oct 8, 2025
5e45310
Address review suggestions
TharmiganK Oct 8, 2025
42156dd
Update integration-tests/tests/resources/samples/masked-logger/Config…
daneshk Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,94 @@ The log module supports contextual logging, allowing you to create loggers with

For more details and advanced usage, see the module specification and API documentation.

## Sensitive Data Masking

The log module provides capabilities to mask sensitive data in log messages to maintain data privacy and security when dealing with personally identifiable information (PII) or other sensitive data.

By default, sensitive data masking is disabled. Enable it in `Config.toml`:

```toml
[ballerina.log]
enableSensitiveDataMasking = true
```

Or configure it per logger:

```ballerina
log:Config secureConfig = {
enableSensitiveDataMasking: true
};
log:Logger secureLogger = log:fromConfig(secureConfig);
```

### Sensitive Data Annotation

Use the `@log:Sensitive` annotation to mark fields in records as sensitive. When such fields are logged, their values will be excluded or masked:

```ballerina
import ballerina/log;

type User record {
string id;
@log:Sensitive
string password;
string name;
};

public function main() {
User user = {id: "U001", password: "mypassword", name: "John Doe"};
log:printInfo("user details", user = user);
}
```

Output (with masking enabled):

```log
time=2025-08-20T09:15:30.123+05:30 level=INFO module="" message="user details" user={"id":"U001","name":"John Doe"}
```

### Masking Strategies

Configure masking strategies using the `strategy` field:

```ballerina
import ballerina/log;

isolated function maskString(string input) returns string {
if input.length() <= 2 {
return "****";
}
return input.substring(0, 1) + "****" + input.substring(input.length() - 1);
}

type User record {
string id;
@log:Sensitive {
strategy: {
replacement: "****"
}
}
string password;
@log:Sensitive {
strategy: {
replacement: maskString
}
}
string ssn;
string name;
};
```

### Masked String Function

Use `log:toMaskedString()` to get the masked version of a value for custom logging implementations:

```ballerina
User user = {id: "U001", password: "mypassword", name: "John Doe"};
string maskedUser = log:toMaskedString(user);
io:println(maskedUser); // {"id":"U001","name":"John Doe"}
```

## Build from the source

### Set up the prerequisites
Expand Down
14 changes: 7 additions & 7 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "log"
version = "2.13.0"
version = "2.14.0"
authors = ["Ballerina"]
keywords = ["level", "format"]
repository = "https://github.com/ballerina-platform/module-ballerina-log"
Expand All @@ -15,18 +15,18 @@ graalvmCompatible = true
[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "log-native"
version = "2.13.0"
path = "../native/build/libs/log-native-2.13.0.jar"
version = "2.14.0"
path = "../native/build/libs/log-native-2.14.0-SNAPSHOT.jar"

[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "log-compiler-plugin"
version = "2.13.0"
path = "../compiler-plugin/build/libs/log-compiler-plugin-2.13.0.jar"
version = "2.14.0"
path = "../compiler-plugin/build/libs/log-compiler-plugin-2.14.0-SNAPSHOT.jar"

[[platform.java21.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "log-test-utils"
version = "2.13.0"
path = "../test-utils/build/libs/log-test-utils-2.13.0.jar"
version = "2.14.0"
path = "../test-utils/build/libs/log-test-utils-2.14.0-SNAPSHOT.jar"
scope = "testOnly"
2 changes: 1 addition & 1 deletion ballerina/CompilerPlugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ id = "log-compiler-plugin"
class = "io.ballerina.stdlib.log.compiler.LogCompilerPlugin"

[[dependency]]
path = "../compiler-plugin/build/libs/log-compiler-plugin-2.13.0.jar"
path = "../compiler-plugin/build/libs/log-compiler-plugin-2.14.0-SNAPSHOT.jar"
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ modules = [
[[package]]
org = "ballerina"
name = "log"
version = "2.13.0"
version = "2.14.0"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
Expand Down
89 changes: 89 additions & 0 deletions ballerina/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,92 @@ The log module supports contextual logging, allowing you to create loggers with
```

For more details and advanced usage, see the module specification and API documentation.

## Sensitive Data Masking

The log module provides capabilities to mask sensitive data in log messages to maintain data privacy and security when dealing with personally identifiable information (PII) or other sensitive data.

By default, sensitive data masking is disabled. Enable it in `Config.toml`:

```toml
[ballerina.log]
enableSensitiveDataMasking = true
```

Or configure it per logger:

```ballerina
log:Config secureConfig = {
enableSensitiveDataMasking: true
};
log:Logger secureLogger = log:fromConfig(secureConfig);
```

### Sensitive Data Annotation

Use the `@log:Sensitive` annotation to mark fields in records as sensitive. When such fields are logged, their values will be excluded or masked:

```ballerina
import ballerina/log;

type User record {
string id;
@log:Sensitive
string password;
string name;
};

public function main() {
User user = {id: "U001", password: "mypassword", name: "John Doe"};
log:printInfo("user details", user = user);
}
```

Output (with masking enabled):

```log
time=2025-08-20T09:15:30.123+05:30 level=INFO module="" message="user details" user={"id":"U001","name":"John Doe"}
```

### Masking Strategies

Configure masking strategies using the `strategy` field:

```ballerina
import ballerina/log;

isolated function maskString(string input) returns string {
if input.length() <= 2 {
return "****";
}
return input.substring(0, 1) + "****" + input.substring(input.length() - 1);
}

type User record {
string id;
@log:Sensitive {
strategy: {
replacement: "****"
}
}
string password;
@log:Sensitive {
strategy: {
replacement: maskString
}
}
string ssn;
string name;
};
```

### Masked String Function

Use `log:toMaskedString()` to get the masked version of a value for custom logging implementations:

```ballerina
User user = {id: "U001", password: "mypassword", name: "John Doe"};
string maskedUser = log:toMaskedString(user);
io:println(maskedUser); // {"id":"U001","name":"John Doe"}
```

35 changes: 31 additions & 4 deletions ballerina/natives.bal
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ public enum FileWriteOption {
#
# + template - The raw template to be processed
# + return - The processed string
#
# # Deprecated
# The `processTemplate` function is deprecated. Use `evaluateTemplate` instead.
@deprecated
public isolated function processTemplate(PrintableRawTemplate template) returns string {
string[] templateStrings = template.strings;
Value[] insertions = template.insertions;
Expand All @@ -183,8 +187,30 @@ public isolated function processTemplate(PrintableRawTemplate template) returns
return result;
}

isolated function processMessage(string|PrintableRawTemplate msg) returns string =>
msg !is string ? processTemplate(msg) : msg;
# Evaluates the raw template and returns the evaluated string.
#
# + template - The raw template to be evaluated
# + enableSensitiveDataMasking - Flag to indicate if sensitive data masking is enabled
# + return - The evaluated string
public isolated function evaluateTemplate(PrintableRawTemplate template, boolean enableSensitiveDataMasking = false) returns string {
string[] templateStrings = template.strings;
Value[] insertions = template.insertions;
string result = templateStrings[0];

foreach int i in 1 ..< templateStrings.length() {
Value insertion = insertions[i - 1];
string insertionStr = insertion is PrintableRawTemplate ?
evaluateTemplate(insertion, enableSensitiveDataMasking) :
insertion is Valuer ?
(enableSensitiveDataMasking ? toMaskedString(insertion()) : insertion().toString()) :
(enableSensitiveDataMasking ? toMaskedString(insertion) : insertion.toString());
result += insertionStr + templateStrings[i];
}
return result;
}

isolated function processMessage(string|PrintableRawTemplate msg, boolean enableSensitiveDataMasking) returns string =>
msg !is string ? evaluateTemplate(msg, enableSensitiveDataMasking) : msg;

# Prints debug logs.
# ```ballerina
Expand Down Expand Up @@ -349,7 +375,7 @@ isolated function fileWrite(string logOutput) {
}
}

isolated function printLogFmt(LogRecord logRecord) returns string {
isolated function printLogFmt(LogRecord logRecord, boolean enableSensitiveDataMasking = false) returns string {
string message = "";
foreach [string, anydata] [k, v] in logRecord.entries() {
string value;
Expand All @@ -367,7 +393,8 @@ isolated function printLogFmt(LogRecord logRecord) returns string {
value = v.toBalString();
}
_ => {
value = v is string ? string `${escape(v.toString())}` : v.toString();
string strValue = enableSensitiveDataMasking ? toMaskedString(v) : v.toString();
value = v is string ? string `${escape(strValue)}` : strValue;
}
}
if message == "" {
Expand Down
23 changes: 17 additions & 6 deletions ballerina/root_logger.bal
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ public type Config record {|
readonly & OutputDestination[] destinations = destinations;
# Additional key-value pairs to include in the log messages. Default is the key-values configured in the module level
readonly & AnydataKeyValues keyValues = {...keyValues};
# Enable sensitive data masking. Default is the module level configuration
boolean enableSensitiveDataMasking = enableSensitiveDataMasking;
|};

type ConfigInternal record {|
LogFormat format = format;
Level level = level;
readonly & OutputDestination[] destinations = destinations;
readonly & KeyValues keyValues = {...keyValues};
boolean enableSensitiveDataMasking = enableSensitiveDataMasking;
|};

final RootLogger rootLogger;
Expand All @@ -59,7 +62,8 @@ public isolated function fromConfig(*Config config) returns Logger|Error {
format: config.format,
level: config.level,
destinations: config.destinations,
keyValues: newKeyValues.cloneReadOnly()
keyValues: newKeyValues.cloneReadOnly(),
enableSensitiveDataMasking: config.enableSensitiveDataMasking
};
return new RootLogger(newConfig);
}
Expand All @@ -71,12 +75,14 @@ isolated class RootLogger {
private final Level level;
private final readonly & OutputDestination[] destinations;
private final readonly & KeyValues keyValues;
private final boolean enableSensitiveDataMasking;

public isolated function init(Config|ConfigInternal config = <Config>{}) {
self.format = config.format;
self.level = config.level;
self.destinations = config.destinations;
self.keyValues = config.keyValues;
self.enableSensitiveDataMasking = config.enableSensitiveDataMasking;
}

public isolated function printDebug(string|PrintableRawTemplate msg, error? 'error, error:StackFrame[]? stackTrace, *KeyValues keyValues) {
Expand Down Expand Up @@ -108,7 +114,8 @@ isolated class RootLogger {
format: self.format,
level: self.level,
destinations: self.destinations,
keyValues: newKeyValues.cloneReadOnly()
keyValues: newKeyValues.cloneReadOnly(),
enableSensitiveDataMasking: self.enableSensitiveDataMasking
};
return new RootLogger(config);
}
Expand All @@ -121,7 +128,7 @@ isolated class RootLogger {
time: getCurrentTime(),
level: logLevel,
module: moduleName,
message: processMessage(msg)
message: processMessage(msg, self.enableSensitiveDataMasking)
};
if err is error {
logRecord.'error = getFullErrorDetails(err);
Expand All @@ -131,7 +138,8 @@ isolated class RootLogger {
select element.toString();
}
foreach [string, Value] [k, v] in keyValues.entries() {
logRecord[k] = v is Valuer ? v() : v is PrintableRawTemplate ? processMessage(v) : v;
logRecord[k] = v is Valuer ? v() :
(v is PrintableRawTemplate ? evaluateTemplate(v, self.enableSensitiveDataMasking) : v);
}
if observe:isTracingEnabled() {
map<string> spanContext = observe:getSpanContext();
Expand All @@ -140,10 +148,13 @@ isolated class RootLogger {
}
}
foreach [string, Value] [k, v] in self.keyValues.entries() {
logRecord[k] = v is Valuer ? v() : v is PrintableRawTemplate ? processMessage(v) : v;
logRecord[k] = v is Valuer ? v() :
(v is PrintableRawTemplate ? evaluateTemplate(v, self.enableSensitiveDataMasking) : v);
}

string logOutput = self.format == JSON_FORMAT ? logRecord.toJsonString() : printLogFmt(logRecord);
string logOutput = self.format == JSON_FORMAT ?
(self.enableSensitiveDataMasking ? toMaskedString(logRecord) : logRecord.toJsonString()) :
printLogFmt(logRecord, self.enableSensitiveDataMasking);

lock {
if outputFilePath is string {
Expand Down
Loading