1641148: Integrate Python and Rust logging (#1052)

* 1641148: Integrate Python and Rust logging

This works by setting up a named pipe between Python and Rust logging.

This requires adding another thread to the Python side to read from the log.
The overhead of that *could* be problematic.

I have no idea if this approach will work on Windows.

This at least *does* work -- any callback-based approach has been ruled
out due to limitations of CFFI or the need to go to a compiling-against-
a-specific-version-of-Python approach (see details in bug).

* hangle -> handle

* Rename and better document function
This commit is contained in:
Michael Droettboom 2020-07-29 14:22:23 -04:00 коммит произвёл GitHub
Родитель e920f25e1f
Коммит 388f78be19
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 196 добавлений и 16 удалений

Просмотреть файл

@ -11,6 +11,7 @@
* Add support for the Counter metric type and Labeled Counter. * Add support for the Counter metric type and Labeled Counter.
* Python * Python
* **Breaking change:** `data_dir` must always be passed to `Glean.initialize`. Prior to this, a missing value would store Glean data in a temporary directory. * **Breaking change:** `data_dir` must always be passed to `Glean.initialize`. Prior to this, a missing value would store Glean data in a temporary directory.
* Logging messages from the Rust core are now sent through Python's standard library `logging` module. Therefore all logging in a Python application can be controlled through the `logging` module interface.
# v31.6.0 (2020-07-24) # v31.6.0 (2020-07-24)

1
Cargo.lock сгенерированный
Просмотреть файл

@ -340,6 +340,7 @@ dependencies = [
"glean-core 31.6.0", "glean-core 31.6.0",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

Просмотреть файл

@ -118,11 +118,7 @@ You can send extra parameters to the `py.test` command by setting the `PYTEST_AR
## Viewing logging output ## Viewing logging output
The Glean Python bindings have two sources of log messages: those that come from Python and those that come from Rust. Log messages (whether originating in Python or Rust) are emitted using the Python standard library's [`logging` module](https://docs.python.org/3/library/logging.html).
### Python log messages
Python log messages are emitted using the Python standard library's [`logging` module](https://docs.python.org/3/library/logging.html).
This module provides a lot of possibilities for customization, but the easiest way to control the log level globally is with [`logging.basicConfig`](https://docs.python.org/3/library/logging.html#logging.basicConfig): This module provides a lot of possibilities for customization, but the easiest way to control the log level globally is with [`logging.basicConfig`](https://docs.python.org/3/library/logging.html#logging.basicConfig):
```python ```python
@ -130,16 +126,6 @@ import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
``` ```
### Rust log messages
Rust log messages are emitted using [`env_logger`](https://docs.rs/env_logger/latest/env_logger/).
The log level can be controlled with the `RUST_LOG` environment variable:
```python
import os
os.environ["RUST_LOG"] = "glean_core=DEBUG"
```
## Linting, formatting and type checking ## Linting, formatting and type checking
The Glean Python bindings use the following tools: The Glean Python bindings use the following tools:

Просмотреть файл

@ -29,6 +29,7 @@ crate-type = ["lib", "staticlib", "cdylib"]
[dependencies] [dependencies]
ffi-support = "0.4.0" ffi-support = "0.4.0"
log = "0.4.8" log = "0.4.8"
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.44" serde_json = "1.0.44"
uuid = { version = "0.8.1", features = ["v4"] } uuid = { version = "0.8.1", features = ["v4"] }
once_cell = "1.2.0" once_cell = "1.2.0"

Просмотреть файл

@ -402,6 +402,20 @@ void glean_destroy_uuid_metric(uint64_t v);
*/ */
void glean_enable_logging(void); void glean_enable_logging(void);
/**
* Initialize the logging system to send JSON messages to a file descriptor
* (Unix) or file handle (Windows).
*
* Not available on Android and iOS.
*
* `fd` is a writable file descriptor (on Unix) or file handle (on Windows).
*
* # Safety
* Unsafe because the fd u64 passed in will be interpreted as either a file
* descriptor (Unix) or file handle (Windows) without any checking.
*/
void glean_enable_logging_to_fd(uint64_t fd);
void glean_event_record(uint64_t metric_id, void glean_event_record(uint64_t metric_id,
uint64_t timestamp, uint64_t timestamp,
RawIntArray extra_keys, RawIntArray extra_keys,

Просмотреть файл

@ -0,0 +1,83 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use std::fs::File;
use std::io::Write;
use std::sync::RwLock;
#[cfg(target_os = "windows")]
use std::os::windows::io::FromRawHandle;
#[cfg(target_os = "windows")]
use std::ffi::c_void;
#[cfg(not(target_os = "windows"))]
use std::os::unix::io::FromRawFd;
use serde::Serialize;
/// An implementation of log::Log that writes log messages in JSON format to a
/// file descriptor/handle. The logging level is ignored in this implementation:
/// it is up to the receiver of these log messages (on the language binding
/// side) to filter the log messages based on their level.
/// The JSON payload of each message in an object with the following keys:
/// - `level` (string): One of the logging levels defined here:
/// https://docs.rs/log/0.4.11/log/enum.Level.html
/// - `message` (string): The logging message.
pub struct FdLogger {
pub file: RwLock<File>,
}
#[derive(Serialize)]
struct FdLoggingRecord {
level: String,
message: String,
}
#[cfg(target_os = "windows")]
unsafe fn get_file_from_fd(fd: u64) -> File {
File::from_raw_handle(fd as *mut c_void)
}
#[cfg(not(target_os = "windows"))]
unsafe fn get_file_from_fd(fd: u64) -> File {
File::from_raw_fd(fd as i32)
}
impl FdLogger {
pub unsafe fn new(fd: u64) -> Self {
FdLogger {
file: RwLock::new(get_file_from_fd(fd)),
}
}
}
impl log::Log for FdLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
// This logger always emits logging messages of any level, and the
// language binding consuming these messages is responsible for
// filtering and routing them.
true
}
fn log(&self, record: &log::Record) {
// Normally, classes implementing the Log trait would filter based on
// the log level here. But in this case, we want to emit all log
// messages and let the logging system in the language binding filter
// and route them.
let payload = FdLoggingRecord {
level: record.level().to_string(),
message: record.args().to_string(),
};
let _ = writeln!(
self.file.write().unwrap(),
"{}",
serde_json::to_string(&payload).unwrap()
);
}
fn flush(&self) {
let _ = self.file.write().unwrap().flush();
}
}

Просмотреть файл

@ -9,6 +9,9 @@ use std::panic::UnwindSafe;
use ffi_support::{define_string_destructor, ConcurrentHandleMap, FfiStr, IntoFfi}; use ffi_support::{define_string_destructor, ConcurrentHandleMap, FfiStr, IntoFfi};
#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
use once_cell::sync::OnceCell;
pub use glean_core::metrics::MemoryUnit; pub use glean_core::metrics::MemoryUnit;
pub use glean_core::metrics::TimeUnit; pub use glean_core::metrics::TimeUnit;
pub use glean_core::upload::ffi_upload_result::*; pub use glean_core::upload::ffi_upload_result::*;
@ -38,6 +41,9 @@ mod timing_distribution;
pub mod upload; pub mod upload;
mod uuid; mod uuid;
#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
mod fd_logger;
use ffi_string_ext::FallibleToString; use ffi_string_ext::FallibleToString;
use from_raw::*; use from_raw::*;
use handlemap_ext::HandleMapExtension; use handlemap_ext::HandleMapExtension;
@ -180,6 +186,39 @@ pub extern "C" fn glean_enable_logging() {
} }
} }
#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
static FD_LOGGER: OnceCell<fd_logger::FdLogger> = OnceCell::new();
/// Initialize the logging system to send JSON messages to a file descriptor
/// (Unix) or file handle (Windows).
///
/// Not available on Android and iOS.
///
/// `fd` is a writable file descriptor (on Unix) or file handle (on Windows).
///
/// # Safety
/// Unsafe because the fd u64 passed in will be interpreted as either a file
/// descriptor (Unix) or file handle (Windows) without any checking.
#[cfg(all(not(target_os = "android"), not(target_os = "ios")))]
#[no_mangle]
pub unsafe extern "C" fn glean_enable_logging_to_fd(fd: u64) {
// Set up logging to a file descriptor/handle. For this usage, the
// language binding should setup a pipe and pass in the descriptor to
// the writing side of the pipe as the `fd` parameter. Log messages are
// written as JSON to the file descriptor.
if FD_LOGGER.set(fd_logger::FdLogger::new(fd)).is_ok() {
// Set the level so everything goes through to the language
// binding side where it will be filtered by the language
// binding's logging system.
if log::set_logger(FD_LOGGER.get().unwrap())
.map(|()| log::set_max_level(log::LevelFilter::Debug))
.is_ok()
{
return;
}
};
}
/// Configuration over FFI. /// Configuration over FFI.
/// ///
/// **CAUTION**: This must match _exactly_ the definition on the Kotlin side. /// **CAUTION**: This must match _exactly_ the definition on the Kotlin side.

Просмотреть файл

@ -402,6 +402,20 @@ void glean_destroy_uuid_metric(uint64_t v);
*/ */
void glean_enable_logging(void); void glean_enable_logging(void);
/**
* Initialize the logging system to send JSON messages to a file descriptor
* (Unix) or file handle (Windows).
*
* Not available on Android and iOS.
*
* `fd` is a writable file descriptor (on Unix) or file handle (on Windows).
*
* # Safety
* Unsafe because the fd u64 passed in will be interpreted as either a file
* descriptor (Unix) or file handle (Windows) without any checking.
*/
void glean_enable_logging_to_fd(uint64_t fd);
void glean_event_record(uint64_t metric_id, void glean_event_record(uint64_t metric_id,
uint64_t timestamp, uint64_t timestamp,
RawIntArray extra_keys, RawIntArray extra_keys,

Просмотреть файл

@ -2,8 +2,12 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import logging
import os
from pathlib import Path from pathlib import Path
import sys import sys
import threading
from typing import Any, List, Optional from typing import Any, List, Optional
import weakref import weakref
@ -31,7 +35,44 @@ _global_weakkeydict: Any = weakref.WeakKeyDictionary()
lib = ffi.dlopen(str(Path(__file__).parent / get_shared_object_filename())) lib = ffi.dlopen(str(Path(__file__).parent / get_shared_object_filename()))
lib.glean_enable_logging()
def setup_logging():
"""
Sets up a pipe for communication of logging messages from the Rust core to
the Python logging system. A thread is created to listen for messages on
the pipe, convert them from JSON and send them to the Python stdlib logging
module.
Must be called after the Glean core has been dlopen'd.
"""
r, w = os.pipe()
lib.glean_enable_logging_to_fd(w)
reader = os.fdopen(r, encoding="utf-8")
name = "glean.rust_core"
log = logging.getLogger(name)
level_map = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
def log_handler():
while True:
data = reader.readline().rstrip()
json_content = json.loads(data)
log.log(level_map.get(json_content["level"], 0), json_content["message"])
logging_thread = threading.Thread(target=log_handler)
logging_thread.daemon = True
logging_thread.start()
setup_logging()
def make_config( def make_config(