Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raw template logging implementation #1157

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
74 changes: 62 additions & 12 deletions ballerina/natives.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,7 +28,16 @@ enum LogLevel {
}

# A value of `anydata` type or a function pointer.
public type Value anydata|Valuer;
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 {
public string[] & readonly strings;
public anydata[] insertions;
};

# A function, which returns `anydata` type.
public type Valuer isolated function () returns anydata;
Expand Down Expand Up @@ -82,19 +91,53 @@ public enum FileWriteOption {
APPEND
}

class PrintableRawTemplateImpl {
*object:RawTemplate;
public Value[] insertions;

public isolated function init(PrintableRawTemplate printableRawTemplate) {
self.strings = printableRawTemplate.strings;
self.insertions = printableRawTemplate.insertions;
}

public isolated function toString() returns string {
Value[] templateInsertions = self.insertions;
string[] templateStrings = self.strings;
string templatedString = templateStrings[0];
foreach int i in 1 ..< (templateStrings.length()) {
Value templateInsert = templateInsertions[i - 1];
if templateInsert is PrintableRawTemplate {
templatedString += new PrintableRawTemplateImpl(templateInsert).toString() + templateStrings[i];
} else if templateInsert is Valuer {
templatedString += templateInsert().toString() + templateStrings[i];
} else {
templatedString += templateInsert.toString() + templateStrings[i];
}
}
return templatedString;
}
}

isolated function processMessage(string|PrintableRawTemplate msg) returns string {
if msg is PrintableRawTemplate {
return new PrintableRawTemplateImpl(msg).toString();
}
return msg;
}

# Prints debug logs.
# ```ballerina
# log:printDebug("debug message", id = 845315)
# log:printDebug(`Debug message with value: ${value}`, id = 845315)
# ```
#
# + msg - The message to be logged
# + '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);
print(DEBUG, processMessage(msg), 'error, stackTrace, keyValues);
}
}

Expand All @@ -108,9 +151,9 @@ 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);
print(ERROR, processMessage(msg), 'error, stackTrace, keyValues);
}
}

Expand All @@ -123,9 +166,9 @@ 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);
print(INFO, processMessage(msg), 'error, stackTrace, keyValues);
}
}

Expand All @@ -138,9 +181,9 @@ 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);
print(WARN, processMessage(msg), 'error, stackTrace, keyValues);
}
}

Expand Down Expand Up @@ -187,7 +230,14 @@ 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;
if v is Valuer {
value = v();
} else if v is PrintableRawTemplate {
value = processMessage(v);
} else {
value = v;
}
logRecord[k] = value;
}
if observe:isTracingEnabled() {
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/Ballerina.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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;
log:printError(string `error: My name is ${myname} and my age is ${myage}`);
log:printWarn(string `warning: My name is ${myname} and my age is ${myage}`);
log:printInfo(string `info: My name is ${myname} and my age is ${myage}`);
log:printDebug(string `debug: My name is ${myname} and my age is ${myage}`);
}

72 changes: 71 additions & 1 deletion integration-tests/tests/tests_logfmt.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -51,6 +53,11 @@ 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_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\\\\\\\"\"";
Expand Down Expand Up @@ -215,6 +222,68 @@ 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(), 8, 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(), 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);
validateLog(logLines[8], MESSAGE_DEBUG_RAW_TEMPLATE_LOGFMT);
}

@test:Config {}
public function testProjectWithoutLogLevelLogfmt() returns error? {
Process|error execResult = exec(bal_exec_path, {}, (), "run", temp_dir_path
Expand Down Expand Up @@ -475,3 +544,4 @@ function exec(@untainted string command, @untainted map<string> env = {},
name: "exec",
'class: "io.ballerina.stdlib.log.testutils.nativeimpl.Exec"
} external;