Code Monkey home page Code Monkey logo

spdlog-rs's Introduction

spdlog-rs

A fast and combinable Rust logging crate, inspired by the C++ logging library spdlog.

Features

  • Very fast (see Benchmarks).
  • Various log targets:
    • Standard streams with optional colors.
    • Files.
    • Rotating log files by file size.
    • Rotating log files hourly.
    • Rotating log files daily.
    • ... (more targets are implementing, PRs are welcome)
    • Extendable with custom log targets.
  • Compatible with log crate (optional).
  • Asynchronous support.
  • Configured via environment variable.
  • Custom formatting.
  • Log filtering - log levels can be modified in runtime as well as in compile time.

Getting started

Add this to Cargo.toml:

[dependencies]
spdlog-rs = "0.3"

The documentation of this crate is hosted on docs.rs, and you can find examples under ./examples directory.

If you have any questions or need help while using this crate, feel free to open a discussion. For feature requests or bug reports, please open an issue.

Supported Rust Versions

The current minimum supported Rust version is 1.56.

spdlog-rs is built against the latest Rust stable release, it is not guaranteed to build on Rust versions earlier than the minimum supported version.

spdlog-rs follows the compiler support policy that the latest stable version and the 3 most recent minor versions before that are always supported. For example, if the current latest Rust stable version is 1.61, the minimum supported version will not be increased past 1.58. Increasing the minimum supported version is not considered a semver breaking change as long as it complies with this policy.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

spdlog-rs's People

Contributors

bpowers avatar dylan-dpc avatar fpervaiz avatar lancern avatar spriteovo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

spdlog-rs's Issues

[Feature Request] Allow calling other macros in logging macros

Is your feature request related to a problem? Please describe.

For anyhow users, patterns like

result.map_err(|err| anyhow!("something went wrong: {err}"));

if x != y {
    do_something_else();
    bail!("something mismatched. {x}!={y}");
}

ensure!(x == y, "something mismatched. {x}!={y}");

are very common in their projects. Some users may also want to log the error message before returning. Currently, they can only do it in this way

result.map_err(|err| {
    error!("something went wrong: {err}")
    anyhow!("something went wrong: {err}")
});

if x != y {
    do_something_else();
    critical!("something mismatched. {x}!={y}");
    bail!("something mismatched. {x}!={y}");
}

This is obviously a bit verbose.

Describe the solution you'd like

Implement a additional optional syntax to the logging macros - log the error message first and then call the specified macro with formatting arguments only.

For syntax, I have some thoughts, but haven't thought hard enough about which is better.

error!(anyhow, "something went wrong: {err}");
error!(then anyhow, "something went wrong: {err}");
error!(then: anyhow, "something went wrong: {err}"); // Because we already have syntax `error!(logger: xxx, "yyy")`
error!(then(anyhow), "something went wrong: {err}");
error!(.then(anyhow), "something went wrong: {err}");

// For future consideration? Call the specified macro first and then log
error!(anyhow then, "something went wrong: {err}");

// Other ideas?

I prefer not to use at least the first one, as it may create ambiguity with future new syntax.

For implementation, it should not be very difficult, just match the new syntax and call the specified macro in the tail of macro.

[Feature Request] Create logger builders from existing loggers

When using loggers in libraries, a common pattern is to build a new Logger object based on the global default logger with the logger's name changed and use that new Logger object as the dedicated logger for the library. However, currently it's difficult to implement similar logic because it's too tedious to build a new logger based on another logger:

pub fn init_my_library() {
    let mut library_logger_builder = LoggerBuilder::new();
    let default_logger = spdlog::default_logger();

    let library_logger = library_logger_builder
        .flush_level_filter(default_logger.flush_level_filter())
        .level_filter(default_logger.level_filter())
        .name("my library")
        .sinks(default_logger.sinks().cloned());
}

I suggest we add a new method to create LoggerBuilder:

impl LoggerBuilder {
    pub fn new_from_logger(logger: &Logger) -> Self;
}

The new_from_logger function creates a new LoggerBuilder object and copies all logger properties (name, log level, sinks, etc.) from the given logger to the new LoggerBuilder object. With this new method, library authors can easily create a dedicated logger for their own libraries and components:

pub fn init_my_library() {
    let library_logger = LoggerBuilder::new_from_logger(&*spdlog::default_logger())
        .name("my library")
        .build();
}

[Feature Request] Event handlers for `FileSink` and `RotatingFileSink`

Implement event handlers (reference from C++ spdlog) for FileSink and RotatingFileSink by adding new methods on the builders of these file sinks.

The names of these methods are tentatively designated as before_file_open, after_file_open, before_file_close and after_file_close.

Unsolved questions:

  • Should we pass a &mut PathBuf to before_file_open handler to allow users to change the file path?

[Bug Report] `AsyncPoolSink` may lose the last log records and panic on program exit

Context: #63


Minimum reproducible example

use std::sync::Arc;

use spdlog::{
    prelude::*,
    sink::{AsyncPoolSink, FileSink},
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    const LOG_FILE: &str = "/tmp/async_file_sink.log";

    let file_sink = Arc::new(FileSink::builder().path(LOG_FILE).truncate(true).build()?);
    let async_pool_sink = Arc::new(AsyncPoolSink::builder().sink(file_sink).build()?);

    let logger = Arc::new(Logger::builder().sink(async_pool_sink).build()?);
    logger.set_flush_level_filter(LevelFilter::All);
    spdlog::set_default_logger(logger);

    info!("hello async_pool_sink");

    // Workaround, adding these 2 lines at the end of `main` function
    //
    // std::thread::sleep(std::time::Duration::from_millis(100));
    // spdlog::default_logger().flush();

    Ok(())
}

Expected to see a log record in /tmp/async_file_sink.log but nothing there.

A guess is because the main function returns before AsyncPoolSink receives the last log task.


Another problem is that if we don't explicitly flush at the end, flush will be called in the atexit callback on program exit then there is a probability of panic.

thread '<unnamed>' panicked at library/std/src/thread/mod.rs:713:35:
use of std::thread::current() is not possible after the thread's local data has been destroyed
stack backtrace:
   0: rust_begin_unwind
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/panicking.rs:72:14
   2: core::panicking::panic_display
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/panicking.rs:178:5
   3: core::panicking::panic_str
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/panicking.rs:152:5
   4: core::option::expect_failed
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/option.rs:1985:5
   5: core::option::Option<T>::expect
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/option.rs:894:21
   6: std::thread::current
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/thread/mod.rs:713:35
   7: crossbeam_channel::waker::current_thread_id::THREAD_ID::__init
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/waker.rs:280:38
   8: crossbeam_channel::waker::current_thread_id::THREAD_ID::__getit::{{closure}}
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys/common/thread_local/fast_local.rs:99:25
   9: std::sys::common::thread_local::lazy::LazyKeyInner<T>::initialize
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys/common/thread_local/mod.rs:54:25
  10: std::sys::common::thread_local::fast_local::Key<T>::try_initialize
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys/common/thread_local/fast_local.rs:190:27
  11: std::sys::common::thread_local::fast_local::Key<T>::get
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys/common/thread_local/fast_local.rs:173:25
  12: crossbeam_channel::waker::current_thread_id::THREAD_ID::__getit
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys/common/thread_local/fast_local.rs:91:21
  13: std::thread::local::LocalKey<T>::try_with
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/thread/local.rs:269:32
  14: crossbeam_channel::waker::current_thread_id
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/waker.rs:283:5
  15: crossbeam_channel::waker::Waker::try_select
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/waker.rs:83:29
  16: crossbeam_channel::waker::SyncWaker::notify
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/waker.rs:224:17
  17: crossbeam_channel::flavors::array::Channel<T>::write
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/flavors/array.rs:226:9
  18: crossbeam_channel::flavors::array::Channel<T>::send
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/flavors/array.rs:343:40
  19: crossbeam_channel::channel::Sender<T>::send
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crossbeam-channel-0.5.11/src/channel.rs:437:42
  20: spdlog::thread_pool::ThreadPool::assign_task
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/thread_pool.rs:68:38
  21: spdlog::sink::async_sink::async_pool_sink::AsyncPoolSink::assign_task
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/sink/async_sink/async_pool_sink.rs:60:9
  22: <spdlog::sink::async_sink::async_pool_sink::AsyncPoolSink as spdlog::sink::Sink>::flush
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/sink/async_sink/async_pool_sink.rs:81:9
  23: spdlog::logger::Logger::flush_sinks::{{closure}}
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/logger.rs:404:31
  24: <core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::for_each
             at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/slice/iter/macros.rs:254:21
  25: spdlog::logger::Logger::flush_sinks
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/logger.rs:403:9
  26: spdlog::logger::Logger::flush
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/logger.rs:160:9
  27: spdlog::flush_default_logger_at_exit::handler
             at ~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.13/src/lib.rs:656:13
  28: <unknown>
  29: exit
  30: <unknown>
  31: __libc_start_main
  32: _start
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
fatal runtime error: failed to initiate panic, error 5
fish: Job 1, 'RUST_BACKTRACE=1 cargo run' terminated by signal SIGABRT (Abort)

This might be a bit hard to fix (the reason may be related to signal-safety, see #18 (comment)), but it shouldn't happen and is worth mentioning here and giving it a try.

[Bug Report] Unnecessary recompiling spdlog-rs

Describe the bug

Running cargo build back to back compiles spdlog-rs on both iterations.

To Reproduce

Steps to reproduce the behavior:

  1. cargo init spdlog-mwe && cd spdlog-mwe && cargo add spdlog-rs

  2. echo "first build" && cargo build && echo "second build" && cargo build

Expected behavior

The first build should download and compile all dependencies. The second build should finish without compiling anything.

Environments

  • OS: Microsoft Windows 11 Business
  • Version: 10.0.22621 N/A Build 22621

Additional context

Running >cargo build --verbose shows that target\debug\build\spdlog-rs-29fb83146f549d72\out\test_utils\common_for_doc_test.rs is dirty.

Dirty spdlog-rs v0.3.11: the file `target\debug\build\spdlog-rs-29fb83146f549d72\out\test_utils\common_for_doc_test.rs` has changed (13341485574.813412900s, 42805400ns after last build at 13341485574.770607500s)

Additionally, has the spdlog-rs{id}/output fil the following lines, which is causing the rebuild.

cargo:rerun-if-changed=<local-path>\target\debug\build\spdlog-rs-29fb83146f549d72\out\test_utils\common_for_doc_test.rs
cargo:rerun-if-changed=<local-path>\target\debug\build\spdlog-rs-29fb83146f549d72\out\test_utils\common_for_integration_test.rs
cargo:rerun-if-changed=<local-path>\target\debug\build\spdlog-rs-29fb83146f549d72\out\test_utils\common_for_unit_test.rs

[Bug Report] Source data is not displayed when using log crate

Describe the bug

Source data is not displayed when using log crate, but displayed properly when using spdlog directly.

To Reproduce

Steps to reproduce the behavior:
main.rs

use spdlog::Logger;
use spdlog::prelude::*;

use std::sync::Arc;
fn main() {
    use_pattern_formatter();
    spdlog::init_log_crate_proxy()
        .expect("users should only call `init_log_crate_proxy` function once");

    let custom_logger: Arc<Logger> = spdlog::default_logger();
    let proxy: &'static spdlog::LogCrateProxy = spdlog::log_crate_proxy();
    proxy.set_logger(Some(custom_logger));
    log::set_max_level(log::LevelFilter::Trace);
    log::info!("No source data");
    info!("With source data");
}

fn use_pattern_formatter() {
    use spdlog::{
        formatter::{pattern, PatternFormatter},
    };
    let new_formatter: Box<PatternFormatter<_>> = Box::new(PatternFormatter::new(pattern!(
        "{date}T{time}.{millisecond}Z [{^{level}}] ({module_path}::{file_name}) {payload}{eol}"
    )));

    for sink in spdlog::default_logger().sinks() {
        sink.set_formatter(new_formatter.clone())
    }
}

Cargo.toml

[package]
name = "log_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
log = "0.4.17"
spdlog-rs = {version = "0.3.7", features = ["source-location", "log"]}

Output

2023-02-05T19:40:42.010Z [info] (::) No source data
2023-02-05T19:40:42.010Z [info] (log_test::main.rs) With source data

Environments

  • OS: Ubuntu
  • Version: 18.04

Tracking issue for pattern formatters

This issue tracks the development progress of pattern formatters. The development takes place under branch pattern-formatter.

  • Basic pattern combinators
  • pattern!
  • Atom patterns (reference: spdlog patterns)
    • Literal string
    • Payload ({v}, {payload})
    • Thread ID ({t}, {tid})
    • Process ID ({p}, {pid})
    • Logger name ({n}, {logger})
    • Log level ({l}, {level})
    • Short log level ({L}, {level-short})
    • Abbreviated weekday name ({a}, {weekday-name})
    • Full weekday name ({A}, {weekday-name-full})
    • Abbreviated month name ({b}, {month-name})
    • Full month name ({B}, {month-name-full})
    • Full date time ({c}, {datetime})
    • 2-digit year ({C}, {year-short})
    • 4-digit year ({Y}, {year})
    • Short date ({D}, {date-short})
    • Month 01-12 ({m}, {month})
    • Day of month 01-31 ({d}, {day})
    • Hours in 24 format 00-23 ({H}, {hour})
    • Hours in 12 format 01-12 ({I}, {hour-12})
    • Minutes 00-59 ({M}, {minute})
    • Seconds 00-59 ({S}, {second})
    • Millisecond part of the current second 000-999 ({e}, {millisecond})
    • Microsecond part of the current second 000000-999999 ({f}, {microsecond})
    • Nanosecond part of the current second 000000000-999999999 ({F}, {nanosecond})
    • AM / PM ({p}, {ampm})
    • 12 hour clock ({r}, {time-12})
    • 24 hour HH:MM time ({R}, {time-short})
    • ISO 8601 time format (HH:MM:SS) ({T}, {X}, {time})
    • ISO 8601 offset from UTC in timezone ([+/-] HH:MM) ({z}, {tz-offset})
    • Seconds since epoch ({E}, {unix})
    • Full formatter ({+}, {full})
    • Color range ({^...$})
    • Source file, line and column ({@}, {loc})
    • Basename of source file ({s}, {source-basename})
    • Full or relative path of the source file ({g}, {source})
    • Source line ({#}, {line})
    • Source column ({%}, {column})

[Feature Request] Support formatting parameters in pattern placeholders

spdlog supports specifying text alignment in its pattern flags, see https://github.com/gabime/spdlog/wiki/3.-Custom-formatting#aligning:

  • %8l requires right alignment and will expand to info;
  • %-8l requires left alignment and will expand to info ;
  • %=8l requires center alignment and will expand to info .

Besides, an additional ! can be used to truncate the expanded result if it's too wide:

  • %3!l, %-3!l, and %=3!l will expand to inf.

This feature can be neat sometimes and maybe we could also support this. We may follow the convention of format! and propose the following new pattern placeholder syntax:

placeholder := '{' NAME [ ':' format_spec ] '}'
format_spec := [ [fill] align ] [ ['.'] width ]
fill := CHAR
align := INT
width := INT

Some examples:

  • {level:8} and {level:<8} will expand to info (left alignment, pad with spaces);
  • {level:-<8} will expand to info---- (left alignment, pad with -);
  • {level:>8} will expand to info (right alignment, pad with spaces);
  • {level:->8} will expand to ----info (right alignment, pad with -);
  • {level:^8} will expand to info (center alignment, pad with spaces);
  • {level:-^8} will expand to --info-- (center alignment, pad with -);
  • {level:.3} will expand to inf (left alignment, truncate field to 3 characters long).

[Bug Report] fails to build on Linux with latest nightly Rust

Describe the bug

fails to build on Linux with latest nightly Rust, stable Rust is fine

To Reproduce

cargo build

Expected behavior

error: non-binding let on a synchronization lock
  --> spdlog/src/periodic_worker.rs:25:22
   |
25 |                 let (_, res) = active
   |                      ^ this lock is not assigned to a binding and is immediately dropped
   |
   = help: consider immediately dropping the value using `drop(..)` after the `let` statement
   = note: `#[deny(let_underscore_lock)]` on by default
help: consider binding to an unused variable to avoid immediately dropping the value
   |
25 |                 let (_unused, res) = active

Screenshots

Environments

  • OS: ubuntu
  • Version: 20

Additional context

Failed to build for iOS

Discussed in #35

Originally posted by newbeeAirfeeen September 6, 2023
I have written an ios sdk now. This sdk uses spdlog-rs. But when I build the sdk of the ios general library. The following command was used:
cargo lipo
The compiler told me that there is no corresponding function on IOS。

spdlog-rs-0.3.10/src/record.rs:291:57
    |
291 |     TID.with(|tid| *tid.borrow_mut().get_or_insert_with(get_current_tid_inner))
    |                                                         ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

I downloaded the source code and checked the relevant calls in libc, and found that there seems to be no corresponding function. If I want to build on ios, is it not supported by the current version yet.
Is it possible to change the way of thinking and use other methods to obtain the thread id when building on ios

Tracking issue for config (deserialization)

I'm considering adding a new feature providing the ability to serialize and deserialize for loggers, that allows users to load logging configs from external inputs (particularly from files) so that they can save the existing configs and/or modify configs dynamically from an external file.

The overall idea is, we depending serde as an optional dependency, and then we derive/implement Serialize & Deserialize around all stuff (e.g. All formatters, sinks, and the Logger)

There are some unsolved issues:

  • What is the best way to serialize trait object types? Should we implement it ourselves or just use erased-serde crate directly? (I have no experience with this crate)
  • How do we handle user-defined sinks and formatters?
  • ...

So this is a discussion issue, anyone could leave your opinion or concern :)


Steps / History

how about upgrade syn package from 1 to 2

Discussed in #43

Originally posted by nneesshh October 23, 2023
I try upgrade syn package from 1 to 2, but failed ...
I don't really know the change betwen 1 and 2, maybe the newest is better?

Can't build with 0.3.9 - undefined reference to `gettid'

Hello,
The build fails with the latest release 0.3.9 with the following error:

 note: /home/alex/development/sdk-coreagent/target/debug/deps/libspdlog-6f3a8729f365763c.rlib(spdlog-6f3a8729f365763c.spdlog.cca83b2da12e547f-cgu.00.rcgu.o): In function `spdlog::record::get_current_tid::get_current_tid_inner':
          /home/alex/.cargo/registry/src/index.crates.io-6f17d22bba15001f/spdlog-rs-0.3.9/src/record.rs:264: undefined reference to `gettid'

OS ubuntu 18.04
Had to force cargo to work with the previous release:

spdlog-rs = {version = "=0.3.8", features = ["source-location", "log"]}

Tracking issue for `runtime_pattern!` macro

As discussed in #45, a RuntimePattern implementation with a pattern registry that can be dynamically built may have security concerns. Thus, while the pattern template string can be evaluated dynamically, the pattern registry, which is basically a map from string to pattern factories, needs to be built at compile time.

We thus propose a new macro runtime_pattern! for building runtime patterns. It can be used as follows:

let pattern: RuntimePattern = runtime_pattern!(template_str, {$custom} => Custom::default);

The only difference between pattern! and runtime_pattern! is that while pattern! only accept a string literal, runtime_pattern! accepts any expressions that evaluate to a string.

This issue tracks the development progress of runtime_pattern!.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.