Skip to content

Commit

Permalink
feat: support journald appender (#80)
Browse files Browse the repository at this point in the history
Signed-off-by: tison <wander4096@gmail.com>
  • Loading branch information
tisonkun authored Nov 14, 2024
1 parent b63071d commit b12f378
Show file tree
Hide file tree
Showing 9 changed files with 731 additions and 2 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

All notable changes to this project will be documented in this file.

## [0.18.0] 2024-11-14

### Breaking changes

* The mapping between syslog severity and log's level is changed.
* `log::Level::Error` is mapped to `syslog::Severity::Error` (unchanged).
* `log::Level::Warn` is mapped to `syslog::Severity::Warning` (unchanged).
* `log::Level::Info` is mapped to `syslog::Severity::Notice` (changed).
* `log::Level::Debug` is mapped to `syslog::Severity::Info` (changed).
* `log::Level::Trace` is mapped to `syslog::Severity::Debug` (unchanged).

### New features

* Add `journald` feature to support journald appenders ([#80](https://github.com/fast/logforth/pull/80)).

## [0.17.1] 2024-11-12

### Refactors
Expand Down
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ rustdoc-args = ["--cfg", "docsrs"]

[features]
fastrace = ["dep:fastrace"]
journald = ["dep:libc"]
json = ["dep:serde_json", "dep:serde", "jiff/serde"]
no-color = ["colored/no-color"]
non-blocking = ["dep:crossbeam-channel"]
Expand All @@ -55,6 +56,7 @@ log = { version = "0.4", features = ["std", "kv_unstable"] }
crossbeam-channel = { version = "0.5", optional = true }
fastrace = { version = "0.7", optional = true }
fasyslog = { version = "0.2", optional = true }
libc = { version = "0.2.162", optional = true }
opentelemetry = { version = "0.27", features = ["logs"], optional = true }
opentelemetry-otlp = { version = "0.27", features = [
"logs",
Expand Down Expand Up @@ -107,3 +109,9 @@ doc-scrape-examples = true
name = "syslog"
path = "examples/syslog.rs"
required-features = ["syslog"]

[[example]]
doc-scrape-examples = true
name = "journald"
path = "examples/journald.rs"
required-features = ["journald"]
32 changes: 32 additions & 0 deletions examples/journald.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024 FastLabs Developers
//
// Licensed 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.

#[cfg(unix)]
fn main() {
use logforth::append::Journald;

let append = Journald::new().unwrap();
logforth::builder().dispatch(|d| d.append(append)).apply();

log::error!("Hello, journald at ERROR!");
log::warn!("Hello, journald at WARN!");
log::info!("Hello, journald at INFO!");
log::debug!("Hello, journald at DEBUG!");
log::trace!("Hello, journald at TRACE!");
}

#[cfg(not(unix))]
fn main() {
println!("This example is only for Unix-like systems.");
}
3 changes: 3 additions & 0 deletions src/append/journald/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Rolling File Appender

This appender is a remix of [tracing-journald](https://crates.io/crates/tracing-journald) and [systemd-journal-logger](https://crates.io/crates/systemd-journal-logger), with several modifications to fit this crate's needs.
195 changes: 195 additions & 0 deletions src/append/journald/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2024 FastLabs Developers
//
// Licensed 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.

// This field is derived from https://github.com/swsnr/systemd-journal-logger.rs/blob/v2.2.0/src/fields.rs.

//! Write well-formatted journal fields to buffers.
use std::fmt::Arguments;
use std::io::Write;

use log::kv::Value;

pub(super) enum FieldName<'a> {
WellFormed(&'a str),
WriteEscaped(&'a str),
}

/// Whether `c` is a valid character in the key of a journal field.
///
/// Journal field keys may only contain ASCII uppercase letters A to Z,
/// numbers 0 to 9 and the underscore.
fn is_valid_key_char(c: char) -> bool {
matches!(c, 'A'..='Z' | '0'..='9' | '_')
}

/// Write an escaped `key` for use in a systemd journal field.
///
/// See [`super::Journald`] for the rules.
fn write_escaped_key(key: &str, buffer: &mut Vec<u8>) {
// Key length is limited to 64 bytes
let mut remaining = 64;

let escaped = key
.to_ascii_uppercase()
.replace(|c| !is_valid_key_char(c), "_");

if escaped.starts_with(|c: char| matches!(c, '_' | '0'..='9')) {
buffer.extend_from_slice(b"ESCAPED_");
remaining -= 8;
}

for b in escaped.into_bytes() {
if remaining == 0 {
break;
}
buffer.push(b);
remaining -= 1;
}
}

fn put_field_name(buffer: &mut Vec<u8>, name: FieldName<'_>) {
match name {
FieldName::WellFormed(name) => buffer.extend_from_slice(name.as_bytes()),
FieldName::WriteEscaped("") => buffer.extend_from_slice(b"EMPTY"),
FieldName::WriteEscaped(name) => write_escaped_key(name, buffer),
}
}

pub(super) trait PutAsFieldValue {
fn put_field_value(self, buffer: &mut Vec<u8>);
}

impl PutAsFieldValue for &[u8] {
fn put_field_value(self, buffer: &mut Vec<u8>) {
buffer.extend_from_slice(self)
}
}

impl PutAsFieldValue for &Arguments<'_> {
fn put_field_value(self, buffer: &mut Vec<u8>) {
match self.as_str() {
Some(s) => buffer.extend_from_slice(s.as_bytes()),
None => {
// SAFETY: no more than an allocate-less version
// buffer.extend_from_slice(format!("{}", self))
write!(buffer, "{}", self).unwrap()
}
}
}
}

impl PutAsFieldValue for Value<'_> {
fn put_field_value(self, buffer: &mut Vec<u8>) {
// SAFETY: no more than an allocate-less version
// buffer.extend_from_slice(format!("{}", self))
write!(buffer, "{}", self).unwrap();
}
}

pub(super) fn put_field_length_encoded<V: PutAsFieldValue>(
buffer: &mut Vec<u8>,
name: FieldName<'_>,
value: V,
) {
put_field_name(buffer, name);
buffer.push(b'\n');
// Reserve the length tag
buffer.extend_from_slice(&[0; 8]);
let value_start = buffer.len();
value.put_field_value(buffer);
let value_end = buffer.len();
// Fill the length tag
let length_bytes = ((value_end - value_start) as u64).to_le_bytes();
buffer[value_start - 8..value_start].copy_from_slice(&length_bytes);
buffer.push(b'\n');
}

pub(super) fn put_field_bytes(buffer: &mut Vec<u8>, name: FieldName<'_>, value: &[u8]) {
if value.contains(&b'\n') {
// Write as length encoded field
put_field_length_encoded(buffer, name, value);
} else {
put_field_name(buffer, name);
buffer.push(b'=');
buffer.extend_from_slice(value);
buffer.push(b'\n');
}
}

#[cfg(test)]
mod tests {
use FieldName::*;

use super::*;

#[test]
fn test_escape_journal_key() {
for case in ["FOO", "FOO_123"] {
let mut bs = vec![];
write_escaped_key(case, &mut bs);
assert_eq!(String::from_utf8_lossy(&bs), case);
}

let cases = vec![
("foo", "FOO"),
("_foo", "ESCAPED__FOO"),
("1foo", "ESCAPED_1FOO"),
("Hallöchen", "HALL_CHEN"),
];
for (key, expected) in cases {
let mut bs = vec![];
write_escaped_key(key, &mut bs);
assert_eq!(String::from_utf8_lossy(&bs), expected);
}

{
for case in [
"very_long_key_name_that_is_longer_than_64_bytes".repeat(5),
"_need_escape_very_long_key_name_that_is_longer_than_64_bytes".repeat(5),
] {
let mut bs = vec![];
write_escaped_key(&case, &mut bs);
println!("{:?}", String::from_utf8_lossy(&bs));
assert_eq!(bs.len(), 64);
}
}
}

#[test]
fn test_put_field_length_encoded() {
let mut buffer = Vec::new();
// See "Data Format" in https://systemd.io/JOURNAL_NATIVE_PROTOCOL/ for this example
put_field_length_encoded(&mut buffer, WellFormed("FOO"), "BAR".as_bytes());
assert_eq!(&buffer, b"FOO\n\x03\0\0\0\0\0\0\0BAR\n");
}

#[test]
fn test_put_field_bytes_no_newline() {
let mut buffer = Vec::new();
put_field_bytes(&mut buffer, WellFormed("FOO"), "BAR".as_bytes());
assert_eq!(&buffer, b"FOO=BAR\n");
}

#[test]
fn test_put_field_bytes_newline() {
let mut buffer = Vec::new();
put_field_bytes(
&mut buffer,
WellFormed("FOO"),
"BAR\nSPAM_WITH_EGGS".as_bytes(),
);
assert_eq!(&buffer, b"FOO\n\x12\0\0\0\0\0\0\0BAR\nSPAM_WITH_EGGS\n");
}
}
Loading

0 comments on commit b12f378

Please sign in to comment.