diff --git a/ changelog.md b/ changelog.md index 2ec1bc87..961e48c6 100644 --- a/ changelog.md +++ b/ changelog.md @@ -1,6 +1,11 @@ # Change Log This file contains all the notable changes done to the Ballerina TCP package through the releases. +## [Unreleased] + +### Added +- [Support logging raw template value in log APIs](https://github.com/ballerina-platform/ballerina-library/issues/3331) + ## [2.5.1] - 2023-01-04 ### Removed diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 03acf776..46802152 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.11.0-20241121-075100-c4c87cbc" +distribution-version = "2201.11.0-20241218-101200-109f6cc7" [[package]] org = "ballerina" diff --git a/ballerina/natives.bal b/ballerina/natives.bal index 5668d051..d40ebdc4 100644 --- a/ballerina/natives.bal +++ b/ballerina/natives.bal @@ -15,9 +15,9 @@ // under the License. import ballerina/io; -import ballerina/observe; import ballerina/jballerina.java; import ballerina/lang.value; +import ballerina/observe; # Represents log level types. enum LogLevel { @@ -27,8 +27,19 @@ enum LogLevel { WARN } -# A value of `anydata` type or a function pointer. -public type Value anydata|Valuer; +# A value of `anydata` type or a function pointer or raw template. +public type Value anydata|Valuer|PrintableRawTemplate; + +# Represents raw templates for logging. +# +# e.g: `The input value is ${val}` +# + strings - String values of the template as an array +# + insertions - Parameterized values/expressions after evaluations as an array +public type PrintableRawTemplate object { + *object:RawTemplate; + public string[] & readonly strings; + public Value[] insertions; +}; # A function, which returns `anydata` type. public type Valuer isolated function () returns anydata; @@ -82,6 +93,30 @@ public enum FileWriteOption { APPEND } +# Process the raw template and return the processed string. +# +# + template - The raw template to be processed +# + return - The processed string +isolated function processTemplate(PrintableRawTemplate template) 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) : + insertion is Valuer ? + insertion().toString() : + insertion.toString(); + result += insertionStr + templateStrings[i]; + } + return result; +} + +isolated function processMessage(string|PrintableRawTemplate msg) returns string => + msg !is string ? processTemplate(msg) : msg; + # Prints debug logs. # ```ballerina # log:printDebug("debug message", id = 845315) @@ -91,7 +126,7 @@ public enum FileWriteOption { # + 'error - The error struct to be logged # + stackTrace - The error stack trace to be logged # + keyValues - The key-value pairs to be logged -public isolated function printDebug(string msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { +public isolated function printDebug(string|PrintableRawTemplate msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { // Added `stackTrace` as an optional param due to https://github.com/ballerina-platform/ballerina-lang/issues/34572 if isLogLevelEnabled(DEBUG, getModuleName(keyValues)) { print(DEBUG, msg, 'error, stackTrace, keyValues); @@ -108,7 +143,7 @@ public isolated function printDebug(string msg, error? 'error = (), error:StackF # + 'error - The error struct to be logged # + stackTrace - The error stack trace to be logged # + keyValues - The key-value pairs to be logged -public isolated function printError(string msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { +public isolated function printError(string|PrintableRawTemplate msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { if isLogLevelEnabled(ERROR, getModuleName(keyValues)) { print(ERROR, msg, 'error, stackTrace, keyValues); } @@ -123,7 +158,7 @@ public isolated function printError(string msg, error? 'error = (), error:StackF # + 'error - The error struct to be logged # + stackTrace - The error stack trace to be logged # + keyValues - The key-value pairs to be logged -public isolated function printInfo(string msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { +public isolated function printInfo(string|PrintableRawTemplate msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { if isLogLevelEnabled(INFO, getModuleName(keyValues)) { print(INFO, msg, 'error, stackTrace, keyValues); } @@ -138,7 +173,7 @@ public isolated function printInfo(string msg, error? 'error = (), error:StackFr # + 'error - The error struct to be logged # + stackTrace - The error stack trace to be logged # + keyValues - The key-value pairs to be logged -public isolated function printWarn(string msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { +public isolated function printWarn(string|PrintableRawTemplate msg, error? 'error = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { if isLogLevelEnabled(WARN, getModuleName(keyValues)) { print(WARN, msg, 'error, stackTrace, keyValues); } @@ -169,12 +204,12 @@ public isolated function setOutputFile(string path, FileWriteOption option = APP } } -isolated function print(string logLevel, string msg, error? err = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { +isolated function print(string logLevel, string|PrintableRawTemplate msg, error? err = (), error:StackFrame[]? stackTrace = (), *KeyValues keyValues) { LogRecord logRecord = { time: getCurrentTime(), level: logLevel, module: getModuleNameExtern() == "." ? "" : getModuleNameExtern(), - message: msg + message: processMessage(msg) }; if err is error { logRecord.'error = getFullErrorDetails(err); @@ -187,7 +222,7 @@ isolated function print(string logLevel, string msg, error? err = (), error:Stac logRecord["stackTrace"] = stackTraceArray; } foreach [string, Value] [k, v] in keyValues.entries() { - anydata value = v is Valuer ? v() : v; + anydata value = v is Valuer ? v() : v is PrintableRawTemplate ? processMessage(v) : v; logRecord[k] = value; } if observe:isTracingEnabled() { diff --git a/integration-tests/Ballerina.toml b/integration-tests/Ballerina.toml index dffd6200..a4b2c935 100644 --- a/integration-tests/Ballerina.toml +++ b/integration-tests/Ballerina.toml @@ -17,4 +17,4 @@ path = "../test-utils/build/libs/log-test-utils-@project.version@.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "io-native" -path = "./lib/io-native-@io.native.version@.jar" +path = "./lib/io-native-@io.native.version@.jar" \ No newline at end of file diff --git a/integration-tests/tests/resources/samples/log-levels-raw-template/main.bal b/integration-tests/tests/resources/samples/log-levels-raw-template/main.bal new file mode 100644 index 00000000..a933fe26 --- /dev/null +++ b/integration-tests/tests/resources/samples/log-levels-raw-template/main.bal @@ -0,0 +1,31 @@ +// Copyright (c) 2021 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. 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/log; + +public function main() { + string myname = "Alex92"; + int myage = 25; + string action1 = "action1"; + string action2 = "action2"; + string action3 = "action3"; + log:printError(`error: My name is ${myname} and my age is ${myage}`); + log:printWarn(`warning: My name is ${myname} and my age is ${myage}`); + log:printInfo(`info: My name is ${myname} and my age is ${myage}`); + log:printDebug(`debug: My name is ${myname} and my age is ${myage}`); + log:printInfo("User details", details = `name: ${myname}, age: ${myage}`, actions = `actions: ${action1}, ${action2}, ${action3}`); +} + diff --git a/integration-tests/tests/tests_logfmt.bal b/integration-tests/tests/tests_logfmt.bal index cb235cc9..6bf9d028 100644 --- a/integration-tests/tests/tests_logfmt.bal +++ b/integration-tests/tests/tests_logfmt.bal @@ -14,8 +14,8 @@ // specific language governing permissions and limitations // under the License. -import ballerina/jballerina.java; import ballerina/io; +import ballerina/jballerina.java; import ballerina/test; const string UTF_8 = "UTF-8"; @@ -25,7 +25,9 @@ const string PRINT_INFO_FILE = "tests/resources/samples/print-functions/info.bal const string PRINT_WARN_FILE = "tests/resources/samples/print-functions/warn.bal"; const string PRINT_DEBUG_FILE = "tests/resources/samples/print-functions/debug.bal"; const string PRINT_ERROR_FILE = "tests/resources/samples/print-functions/error.bal"; +const string PRINT_RAW_TEMPLATE_FILE = "tests/resources/samples/print-functions/raw_template.bal"; const string LOG_LEVEL_FILE = "tests/resources/samples/log-levels/main.bal"; +const string LOG_LEVEL_RAW_TEMPLATE_FILE = "tests/resources/samples/log-levels-raw-template/main.bal"; const string FILE_WRITE_OUTPUT_OVERWRITE_INPUT_FILE_LOGFMT = "tests/resources/samples/file-write-output/single-file/overwrite-logfmt.bal"; const string FILE_WRITE_OUTPUT_APPEND_INPUT_FILE_LOGFMT = "tests/resources/samples/file-write-output/single-file/append-logfmt.bal"; @@ -51,6 +53,12 @@ const string MESSAGE_WARN_LOGFMT = " level=WARN module=\"\" message=\"warn log\" const string MESSAGE_INFO_LOGFMT = " level=INFO module=\"\" message=\"info log\""; const string MESSAGE_DEBUG_LOGFMT = " level=DEBUG module=\"\" message=\"debug log\""; +const string MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT = " level=ERROR module=\"\" message=\"error: My name is Alex92 and my age is 25\""; +const string MESSAGE_WARN_RAW_TEMPLATE_LOGFMT = " level=WARN module=\"\" message=\"warning: My name is Alex92 and my age is 25\""; +const string MESSAGE_INFO_RAW_TEMPLATE_LOGFMT = " level=INFO module=\"\" message=\"info: My name is Alex92 and my age is 25\""; +const string MESSAGE_DEBUG_RAW_TEMPLATE_LOGFMT = " level=DEBUG module=\"\" message=\"debug: My name is Alex92 and my age is 25\""; +const string MESSAGE_KEY_VALUE_PAIR_LOGFMT = " level=INFO module=\"\" message=\"User details\" details=\"name: Alex92, age: 25\" actions=\"actions: action1, action2, action3\""; + const string MESSAGE_ERROR_MAIN_LOGFMT = " level=ERROR module=myorg/myproject message=\"error log\\t\\n\\r\\\\\\\"\""; const string MESSAGE_WARN_MAIN_LOGFMT = " level=WARN module=myorg/myproject message=\"warn log\\t\\n\\r\\\\\\\"\""; const string MESSAGE_INFO_MAIN_LOGFMT = " level=INFO module=myorg/myproject message=\"info log\\t\\n\\r\\\\\\\"\""; @@ -215,6 +223,86 @@ public function testDebugLevelLogfmt() returns error? { validateLog(logLines[8], MESSAGE_DEBUG_LOGFMT); } +@test:Config {} +public function testErrorLevelRawTemplateLogfmt() returns error? { + Process|error execResult = exec(bal_exec_path, {BAL_CONFIG_FILES: CONFIG_ERROR_LOGFMT}, (), "run", LOG_LEVEL_RAW_TEMPLATE_FILE); + Process result = check execResult; + int _ = check result.waitForExit(); + int _ = check result.exitCode(); + io:ReadableByteChannel readableResult = result.stderr(); + io:ReadableCharacterChannel sc = new (readableResult, UTF_8); + string outText = check sc.read(100000); + string[] logLines = re`\n`.split(outText.trim()); + test:assertEquals(logLines.length(), 6, INCORRECT_NUMBER_OF_LINES); + validateLog(logLines[5], MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT); +} + +@test:Config {} +public function testWarnLevelRawTemplateLogfmt() returns error? { + Process|error execResult = exec(bal_exec_path, {BAL_CONFIG_FILES: CONFIG_WARN_LOGFMT}, (), "run", LOG_LEVEL_RAW_TEMPLATE_FILE); + Process result = check execResult; + int _ = check result.waitForExit(); + int _ = check result.exitCode(); + io:ReadableByteChannel readableResult = result.stderr(); + io:ReadableCharacterChannel sc = new (readableResult, UTF_8); + string outText = check sc.read(100000); + string[] logLines = re`\n`.split(outText.trim()); + test:assertEquals(logLines.length(), 7, INCORRECT_NUMBER_OF_LINES); + validateLog(logLines[5], MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[6], MESSAGE_WARN_RAW_TEMPLATE_LOGFMT); +} + +@test:Config {} +public function testInfoLevelRawTemplateLogfmt() returns error? { + Process|error execResult = exec(bal_exec_path, {BAL_CONFIG_FILES: CONFIG_INFO_LOGFMT}, (), "run", LOG_LEVEL_RAW_TEMPLATE_FILE); + Process result = check execResult; + int _ = check result.waitForExit(); + int _ = check result.exitCode(); + io:ReadableByteChannel readableResult = result.stderr(); + io:ReadableCharacterChannel sc = new (readableResult, UTF_8); + string outText = check sc.read(100000); + string[] logLines = re`\n`.split(outText.trim()); + test:assertEquals(logLines.length(), 9, INCORRECT_NUMBER_OF_LINES); + validateLog(logLines[5], MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[6], MESSAGE_WARN_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[7], MESSAGE_INFO_RAW_TEMPLATE_LOGFMT); +} + +@test:Config {} +public function testDebugLevelRawTemplateLogfmt() returns error? { + Process|error execResult = exec(bal_exec_path, {BAL_CONFIG_FILES: CONFIG_DEBUG_LOGFMT}, (), "run", LOG_LEVEL_RAW_TEMPLATE_FILE); + Process result = check execResult; + int _ = check result.waitForExit(); + int _ = check result.exitCode(); + io:ReadableByteChannel readableResult = result.stderr(); + io:ReadableCharacterChannel sc = new (readableResult, UTF_8); + string outText = check sc.read(100000); + string[] logLines = re`\n`.split(outText.trim()); + test:assertEquals(logLines.length(), 10, INCORRECT_NUMBER_OF_LINES); + validateLog(logLines[5], MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[6], MESSAGE_WARN_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[7], MESSAGE_INFO_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[8], MESSAGE_DEBUG_RAW_TEMPLATE_LOGFMT); +} +@test:Config {} +public function testRawTemplateKeyValuePair() returns error? { + Process|error execResult = exec(bal_exec_path, {BAL_CONFIG_FILES: CONFIG_DEBUG_LOGFMT}, (), "run", LOG_LEVEL_RAW_TEMPLATE_FILE); + Process result = check execResult; + int _ = check result.waitForExit(); + int _ = check result.exitCode(); + io:ReadableByteChannel readableResult = result.stderr(); + io:ReadableCharacterChannel sc = new (readableResult, UTF_8); + string outText = check sc.read(100000); + string[] logLines = re`\n`.split(outText.trim()); + test:assertEquals(logLines.length(), 10, INCORRECT_NUMBER_OF_LINES); + validateLog(logLines[5], MESSAGE_ERROR_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[6], MESSAGE_WARN_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[7], MESSAGE_INFO_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[8], MESSAGE_DEBUG_RAW_TEMPLATE_LOGFMT); + validateLog(logLines[9], MESSAGE_KEY_VALUE_PAIR_LOGFMT); +} + + @test:Config {} public function testProjectWithoutLogLevelLogfmt() returns error? { Process|error execResult = exec(bal_exec_path, {}, (), "run", temp_dir_path @@ -475,3 +563,4 @@ function exec(@untainted string command, @untainted map env = {}, name: "exec", 'class: "io.ballerina.stdlib.log.testutils.nativeimpl.Exec" } external; +