Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 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
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
17 changes: 9 additions & 8 deletions ballerina/natives.bal
Original file line number Diff line number Diff line change
Expand Up @@ -166,25 +166,25 @@ public enum FileWriteOption {
#
# + template - The raw template to be processed
# + return - The processed string
public isolated function processTemplate(PrintableRawTemplate template) returns string {
public isolated function processTemplate(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 ?
processTemplate(insertion) :
processTemplate(insertion, enableSensitiveDataMasking) :
insertion is Valuer ?
insertion().toString() :
insertion.toString();
(enableSensitiveDataMasking ? toMaskedString(insertion()) : insertion().toString()) :
(enableSensitiveDataMasking ? toMaskedString(insertion) : insertion.toString());
result += insertionStr + templateStrings[i];
}
return result;
}

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

# Prints debug logs.
# ```ballerina
Expand Down Expand Up @@ -349,7 +349,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 +367,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 ? processMessage(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 ? processMessage(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
56 changes: 56 additions & 0 deletions ballerina/sensitive_data_masking.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/jballerina.java;

# Exclude the field from log output
public const EXCLUDE = "EXCLUDE";

# Replacement function type for sensitive data masking
public type ReplacementFunction isolated function (string input) returns string;

# Replacement strategy for sensitive data
#
# + replacement - The replacement value. This can be a string which will be used to replace the
# entire value, or a function that takes the original value and returns a masked version.
public type Replacement record {|
string|ReplacementFunction replacement;
|};

# Masking strategy for sensitive data
public type MaskingStrategy EXCLUDE|Replacement;

# Represents sensitive data with a masking strategy
#
# + strategy - The masking strategy to apply (default: EXCLUDE)
public type SensitiveDataConfig record {|
MaskingStrategy strategy = EXCLUDE;
|};

# Marks a record field or type as sensitive, excluding it from log output
#
# + strategy - The masking strategy to apply (default: EXCLUDE)
public annotation SensitiveDataConfig SensitiveData on record field;

configurable boolean enableSensitiveDataMasking = false;

# Returns a masked string representation of the given data based on the sensitive data masking configuration.
#
# + data - The data to be masked
# + return - The masked string representation of the data
public isolated function toMaskedString(anydata data) returns string = @java:Method {
'class: "io.ballerina.stdlib.log.Utils"
} external;
84 changes: 84 additions & 0 deletions ballerina/tests/log_masking.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/io;
import ballerina/test;

final Logger maskerJsonLogger = check fromConfig(enableSensitiveDataMasking = true);
final Logger maskerLogger = check fromConfig(enableSensitiveDataMasking = true, format = LOGFMT);

string[] maskedLogs = [];

function addMaskedLogs(io:FileOutputStream fileOutputStream, io:Printable... values) {
var firstValue = values[0];
if firstValue is string {
maskedLogs.push(firstValue);
}
}

final readonly & User user = {
name: "John Doe",
ssn: "123-45-6789",
password: "password123",
mail: "john.doe@example.com",
creditCard: "4111-1111-1111-1111"
};

isolated function getUser() returns User => user;

@test:Config {
groups: ["logMasking"]
}
function testLogMasking() returns error? {
test:when(mock_fprintln).call("addMaskedLogs");
maskerJsonLogger.printInfo("user logged in", user = user);
string expectedLog = string `"message":"user logged in","user":{"name":"John Doe","password":"*****","mail":"joh**************com"},"env":"test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();

maskerLogger.printInfo("user logged in", user = user);
expectedLog = string `message="user logged in" user={"name":"John Doe","password":"*****","mail":"joh**************com"} env="test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();

map<json> userTmp = user;
maskerJsonLogger.printInfo("user logged in", user = userTmp);
expectedLog = string `"message":"user logged in","user":{"name":"John Doe","password":"*****","mail":"joh**************com"},"env":"test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();

userTmp = check user.cloneWithType();
maskerJsonLogger.printInfo("user logged in", user = userTmp);
expectedLog = string `"message":"user logged in","user":{"name":"John Doe","ssn":"123-45-6789","password":"password123","mail":"john.doe@example.com","creditCard":"4111-1111-1111-1111"},"env":"test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();

maskerLogger.printDebug(`user login event. user details: ${user}`);
expectedLog = string `message="user login event. user details: {\"name\":\"John Doe\",\"password\":\"*****\",\"mail\":\"joh**************com\"}" env="test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();

maskerLogger.printWarn("user login attempt failed", user = getUser);
expectedLog = string `message="user login attempt failed" user={"name":"John Doe","password":"*****","mail":"joh**************com"} env="test"`;
test:assertEquals(maskedLogs.length(), 1);
test:assertTrue(maskedLogs[0].includes(expectedLog));
maskedLogs.removeAll();
}
3 changes: 3 additions & 0 deletions ballerina/tests/logger_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ configurable Config loggerConfig2 = {};

type Context record {|
string id;
// By default root logger is configured not to mask sensitive data
// So this is added as a negative test case
@SensitiveData
string msg;
|};

Expand Down
Loading