GithubHelp home page GithubHelp logo

serilog-contrib / serilogsinksinmemory Goto Github PK

View Code? Open in Web Editor NEW
53.0 6.0 7.0 133 KB

In-memory sink for Serilog to use for testing

License: MIT License

C# 100.00%
serilog-sink fluentassertions

serilogsinksinmemory's Introduction

Serilog.Sinks.InMemory

In-memory sink for Serilog to use for testing with FluentAssertions support for easy to write assertions.

Build status

build-and-test release

NuGet Serilog.Sinks.InMemory NuGet Serilog.Sinks.InMemory.Assertions

Usage

To just use the sink, add the Serilog.Sinks.InMemory NuGet package:

dotnet CLI:

dotnet add package Serilog.Sinks.InMemory

PowerShell:

Install-Package Serilog.Sinks.InMemory

But it's better with assertions so you'll also want to add the Serilog.Sinks.InMemory.Assertions NuGet package:

dotnet CLI:

dotnet add package Serilog.Sinks.InMemory.Assertions

PowerShell:

Install-Package Serilog.Sinks.InMemory.Assertions

Example

Let's say you have a class with method implementing some complicated business logic:

public class ComplicatedBusinessLogic
{
    private readonly ILogger _logger;

    public ComplicatedBusinessLogic(ILogger logger)
    {
        _logger = logger;
    }

    public string FirstTenCharacters(string input)
    {
        return input.Substring(0, 10);
    }
}

A request came in to log a message with the number of characters in the input. So to test that you can create a mock of ILogger and assert the method to log was called, however mock setups quickly become very messy (true: this is my opinion!) and assertions on mocks have the same problem when you start verifying values of arguments.

So instead let's use Serilog and a dedicated sink for testing:

public class WhenExecutingBusinessLogic
{
    public void GivenInputOfFiveCharacters_MessageIsLogged()
    {
        var logger = new LoggerConfiguration()
            .WriteTo.InMemory()
            .CreateLogger();

        var logic = new ComplicatedBusinessLogic(logger);

        logic.FirstTenCharacters("12345");

        // Use the static Instance property to access the in-memory sink
        InMemorySink.Instance
            .Should()
            .HaveMessage("Input is {count} characters long");
    }
}

The test will now fail with Expected a message to be logged with template \"Input is {count} characters long\" but didn't find any

Now change the implementation to:

public string FirstTenCharacters(string input)
{
    _logger.Information("Input is {count} characters long", input.Length);

    return input.Substring(0, 10);
}

Run the test again and it now passes. But how do we ensure this message is only logged once?

To do that, create a new test like so:

public void GivenInputOfFiveCharacters_MessageIsLoggedOnce()
{
    /* omitted for brevity */

    InMemorySink.Instance
        .Should()
        .HaveMessage("Input is {count} characters long")
        .Appearing().Once();
}

To verify if a message is logged multiple times use Appearing().Times(int numberOfTimes)

So now you'll want to verify that the property count has the expected value. This builds upon the previous test:

public void GivenInputOfFiveCharacters_CountPropertyValueIsFive()
{
    /* omitted for brevity */

    InMemorySink.Instance
        .Should()
        .HaveMessage("Input is {count} characters long")
        .Appearing().Once()
        .WithProperty("count")
        .WithValue(5);
}

Asserting a message appears more than once

Let's say you have a log message in a loop and you want to verify that:

public void GivenLoopWithFiveItems_MessageIsLoggedFiveTimes()
{
    /* omitted for brevity */

    InMemorySink.Instance
        .Should()
        .HaveMessage("Input is {count} characters long")
        .Appearing().Times(5);
}

Asserting a message has a certain level

Apart from a message being logged, you'll also want to verify it is of the right level. You can do that using the WithLevel() assertion:

public void GivenLoopWithFiveItems_MessageIsLoggedFiveTimes()
{
    /* omitted for brevity */

    InMemorySink.Instance
        .Should()
        .HaveMessage("Input is {count} characters long")
        .Appearing().Once()
        .WithLevel(LogEventLevel.Information);
}

This also works for multiple messages:

public void GivenLoopWithFiveItems_MessageIsLoggedFiveTimes()
{
    logger.Warning("Test message");
    logger.Warning("Test message");
    logger.Warning("Test message");

    InMemorySink.Instance
        .Should()
        .HaveMessage("Test message")
        .Appearing().Times(3)
        .WithLevel(LogEventLevel.Information);
}

This will fail with a message: Expected instances of log message "Hello, world!" to have level Information, but found 3 with level Warning

Asserting messages with a pattern

Instead of matching on the exact message you can also match on a certain pattern using the Containing() assertion:

InMemorySink.Instance
   .Should()
   .HaveMessage()
   .Containing("some pattern")
   .Appearing().Once();

which matches on log messages:

  • this is some pattern
  • some pattern in a message
  • this is some pattern in a message

Asserting messages have been logged at all (or not!)

When you want to assert that a message has been logged but don't care about what message you can do that with HaveMessage and Appearing:

InMemorySink.Instance
    .Should()
    .HaveMessage()
    .Appearing().Times(3); // Expect three messages to be logged

and of course the inverse is also possible when expecting no messages to be logged:

InMemorySink.Instance
    .Should()
    .NotHaveMessage();

or that a specific message is not be logged

InMemorySink.Instance
    .Should()
    .NotHaveMessage("a specific message");

Asserting properties on messages

When you want to assert that a message has a property you can do that using the WithProperty assertion:

InMemorySink.Instance
    .Should()
    .HaveMessage("Message with {Property}")
    .Appearing().Once()
    .WithProperty("Property");

To then assert that it has a certain value you would use WithValue:

InMemorySink.Instance
    .Should()
    .HaveMessage("Message with {Property}")
    .Appearing().Once()
    .WithProperty("Property")
    .WithValue("property value");

Asserting that a message has multiple properties can be accomplished using the And constraint:

InMemorySink.Instance
    .Should()
    .HaveMessage("Message with {Property1} and {Property2}")
    .Appearing().Once()
    .WithProperty("Property1")
    .WithValue("value 1")
    .And
    .WithProperty("Property2")
    .WithValue("value 2");

When you have a log message that appears a number of times and you want to assert that the value of the log property has the expected values you can do that using the WithValues assertion:

InMemorySink.Instance
    .Should()
    .HaveMessage("Message with {Property1} and {Property2}")
    .Appearing().Times(3)
    .WithProperty("Property1")
    .WithValue("value 1", "value 2", "value 3")

Note: WithValue takes an array of values.

Sometimes you might want to use assertions like BeLessThanOrEqual() or HaveLength() and in those cases WithValue is not very helpful. Instead you can use WhichValue<T>() to access the value of the log property:

InMemorySink.Instance
    .Should()
    .HaveMessage()
    .Appearing().Once()
    .WithProperty("PropertyOne")
    .WhichValue<string>()
    .Should()
    .HaveLength(3);

If the type of the value of the log property does not match the generic type parameter the WhichValue<T> method will throw an exception.

Note: This only works for scalar values. When you pass an object as the property value when logging a message Serilog converts that into a string.

Asserting a property with a destructured object

If you use object destructuring:

var someObject = new { Foo = "bar", Baz = "quux" };
logger.Information("Hello {@SomeObject}", someObject);

and want to assert on properties of the destructured object you can use the HavingADestructuredObject() assertion like so:

InMemorySink.Instance
    .Should()
    .HaveMessage("Hello {@SomeObject}")
    .Appearing().Once()
    .WithProperty("SomeObject")
    .HavingADestructuredObject()
    .WithProperty("Foo")
    .WithValue("bar");

When the property SomeObject doesn't hold a destructured object the assertion will fail with the message: "Expected message "Hello {NotDestructured}" to have a property "NotDestructured" that holds a destructured object but found a scalar value"

Clearing log events between tests

Depending on your test framework and test setup you may want to ensure that the log events captured by the InMemorySink are cleared so tests are not interfering with eachother. To enable this, the InMemorySink implements the IDisposable interface. When Dispose() is called the LogEvents collection is cleared.

It will depend on the test framework or your test if you need this feature. With xUnit this feature is not necessary as it isolates each test in its own instance of the test class which means that they all have their own instance of the InMemorySink. MSTest however has a different approach and there you may want to use this feature as follows:

[TestClass]
public class WhenDemonstratingDisposableFeature
{
    private Logger _logger;

    [TestInitialize]
    public void Initialize()
    {
        _logger?.Dispose();

        _logger = new LoggerConfiguration()
            .WriteTo.InMemory()
            .CreateLogger();
    }

    [TestMethod]
    public void GivenAFoo_BarIsBlah()
    {
        _logger.Information("Foo");

        InMemorySink.Instance
            .Should()
            .HaveMessage("Foo");
    }

    [TestMethod]
    public void GivenABar_BazIsQuux()
    {
        _logger.Information("Bar");

        InMemorySink.Instance
            .Should()
            .HaveMessage("Bar");
    }
}

this approach ensures that the GivenABar_BazIsQuux does not see any messages logged in a previous test.

Creating a logger

Loggers are created using a LoggerConfiguration object. A default initiation would be as follows:

var logger = new LoggerConfiguration()
    .WriteTo.InMemory()
    .CreateLogger();

Output templates

Text-based sinks use output templates to control formatting. this can be modified through the outputTemplate parameter:

var logger = new LoggerConfiguration()
    .WriteTo.InMemory(outputTemplate: "{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

The default template, shown in the example above, uses built-in properties like Timestamp and Level. Refer to the offcial documentation for further configuration and explanation of these properties.

Minimum level

In this example only Information level logs and higher will be written to the InMemorySink.

var logger = new LoggerConfiguration()
    .WriteTo.InMemory(restrictedToMinimumLevel: Events.LogEventLevel.Information)
    .CreateLogger();

Default Level - if no MinimumLevel is specified, then Verbose level events and higher will be processed.

Dynamic levels

If an app needs dynamic level switching, the first step is to create an instance of LoggingLevelSwitch when the logger is being configured:

var levelSwitch = new LoggingLevelSwitch();

This object defaults the current minimum level to Information, so to make logging more restricted, set its minimum level up-front:

levelSwitch.MinimumLevel = LogEventLevel.Warning;

When configuring the logger, provide the switch using MinimumLevel.ControlledBy():

var log = new LoggerConfiguration()
    .MinimumLevel.ControlledBy(levelSwitch)
    .WriteTo.InMemory()
    .CreateLogger();

Now, events written to the logger will be filtered according to the switchโ€™s MinimumLevel property.

To turn the level up or down at runtime, perhaps in response to a command sent over the network, change the property:

levelSwitch.MinimumLevel = LogEventLevel.Verbose;
log.Verbose("This will now be logged");

serilogsinksinmemory's People

Contributors

augustoproiete avatar mderriey avatar sandermvanvliet 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

serilogsinksinmemory's Issues

FluentAssertions interface changed again - MissingMethodException

Having upgraded FluentAssertions to the latest version, 5.6.0, I get a MissingMethodException because the IAssertionScope changes introduced in 5.5.0 (fluentassertions/fluentassertions@fa354f8#diff-9ce3e8a24102d4be5af20404369fbd16L126) were reverted in 5.5.3 (fluentassertions/fluentassertions@9a36498#diff-9ce3e8a24102d4be5af20404369fbd16L95):

Method not found: 'FluentAssertions.Execution.IAssertionScope FluentAssertions.Execution.AssertionScope.BecauseOf(System.String, System.Object[])'. at Serilog.Sinks.InMemory.Assertions.InMemorySinkAssertions.HaveMessage(String messageTemplate, String because, Object[] becauseArgs)

I think that it should be sufficient to just rebuild the nuget package with no source code change, as it looks like you will automatically pull in and build against the latest 5.x version.

`WithException` assertion for comparing logged exception within specific message

suggestion:

            var data = new Exception("Example error exception");
            _logger.Information(data, "Example error");

            InMemorySink.Instance
                .Should()
                .HaveMessage("Example error with sensitive data")
                .WithException(data) //<-- addition
                .WithException(assertions=> assertions //<-- another variant
                   .WithMessage(data.Message)
                   .WithData("somekey",data["somekey"])
                )

Mention in the README.md that InMemorySink.Instance is AsyncLocal

It is not mentioned in the README.md that InMemorySink.Instance is actually AsyncLocal which is quite important piece of information. I thought that InMemorySink.Instance is buggy when it was not behaving as I was expected: Instance is typically a singleton. I only discovered that important fact through issues.

Also you could mention that it is possibly important if you mix InMemorySink with integration tests based on WebApplicationFactory, that was my use-case where the AsyncLocal completely surprised me.

BTW. So far I love it, thank you for your contribution, InMemorySink combined with the assertion library turns tests into little things of beauty :)

Use from a controller?

Hi,

I try to use from a controller but InMemorySink.Instance.LogEvents seems always empty, does I need to configure any DI?

Regards

`RenderMessage` does not render same message as serilog does in sinks.

issue: RenderMessage does not render same message as serilog does in sinks.

Example test body:

            var data = new Exception("XXX");
            _logger.Information(data, "YYY");
            

            var logEvent = InMemorySink.Instance.LogEvents.SingleOrDefault();
            logEvent.Should().NotBeNull();
            var message = logEvent!.RenderMessage();
            
            message
                .Should()
                .Contain("XXX"); //<-- FAILURE. 

Log message in any other sink will look like that:

[09:18:41 INF] YYY
System.Exception: XXX

Within in-memory sink after RenderMessage() called:

YYY

Does it support LogContext?

I'm trying to test that my method sets context properties. For example, it does
LogContext.PushProperty(DiagnosticField.BoxId, "abcd");
....
Logger.Information("Message processed successfully");

then I'm trying to check if it was set properly, using this:

InMemorySink.Instance
                  .Should()
                  .HaveMessage("Message processed successfully").Appearing().Once()
                  .WithProperty(DiagnosticField.BoxId).WithValue("abcd");

I got the following error:


  ---- Expected message "Message processed successfully" to have a property "BoxId" but it wasn't found
  Stack Trace: 
    ----- Inner Stack Trace -----
    XUnit2TestFramework.Throw(String message)
    TestFrameworkProvider.Throw(String message)
    DefaultAssertionStrategy.HandleFailure(String message)
    AssertionScope.FailWith(Func`1 failReasonFunc)
    AssertionScope.FailWith(Func`1 failReasonFunc)
    AssertionScope.FailWith(String message, Object[] args)
    LogEventAssertion.WithProperty(String name, String because, Object[] becauseArgs)
    <>c__DisplayClass9_0.<PositiveProcessingTest>b__1() line 148
    Testable.evaluate[a,b](FSharpFunc`2 body, a a)```

It looks like LogContext is not supported. Am I wrong? Please help. Thanks

Make Inmemory Instance disposable

Thank you very much for this component. I currently use it keep a register of issues I found while I am parsing some text files.

Since the Instance object is read only, it is not possible to clear the list of events logged. Could it be possible to add a IDisposable interface so it can be called from Serilog SaveandClose method and implement the Dispose method to reset the EventList?

Thanks in advance.

What is the best way to check for properties in multiple messages?

What is the best way to check for properties in multiple messages?

I would like to write something similar to

InMemorySink.Instance.Should()
            .HaveMessage("HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms")
            .WithLevel(LogEventLevel.Information)
            .Appearing().Times(2)
            .WithProperty("CorrelationId").BeEquivalentTo(new string[] { "CorrelationId1", "CorrelationId2"});  // Something like this.

HaveMessage throws `System.InvalidOperationException : Collection was modified; enumeration operation may not execute.` exception

The following line in unit test

InMemorySink.Instance.Should().HaveMessage(message);

sometimes throws an exception as follows:

System.InvalidOperationException : Collection was modified; enumeration operation may not execute.
  Stack Trace:
     at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.WhereEnumerableIterator`1.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Serilog.Sinks.InMemory.Assertions.InMemorySinkAssertions.HaveMessage(String messageTemplate, String because, Object[] becauseArgs)

I guess it will be fixed if .ToList is followed by .Where:
https://github.com/sandermvanvliet/SerilogSinksInMemory/blob/master/src/Serilog.Sinks.InMemory.Assertions/InMemorySinkAssertions.cs#L23

Create a thread-safe InMemorySink

We would like to create a thread-safe InMemorySink for the use in our projects. At the moment, when we make a few parallel async calls, the logger does not have all the log events.

We see two possible solutions:

  1. Add a lock object to make all methods thread-safe in the existing InMemorySink class.
  2. Same as above but in a new thread-safe class which can be accessed by a new builder method in the InMemorySinkExtensions class.

Please let me know which one is a preferred option and we are happy to raise a pull request.

Found version 1.0.0 in nuget.org package source that is not current with documentation

Hello, I am happy to find this library fit for my testing requirements, however, I first installed a version 1.0.0 from nuget.org via Visual Studio. It has a date published of May 23rd, 2024 (today) and the correct contact information for this library. A corresponding Serilog.Sinks.InMemory.Assertions 1.0.0 version is also available. Neither version has the supported API in documentation and tests matching the release 0.11.0 version on GitHub. Not sure what gives but could be someone spoofing the library! See linked screenshot.

Serilog Sinks InMemory Nuget Version info

InMemorySink doesn't see log entries

I have a plugin architecture where a half dozen plugins correctly find InMemorySink.Instance containing log entries...as expected. However, I have one plugin that uses the exact same code...but InMemorySink.Instance is empty. The screenshot below shows that the inmemory sink has indeed collected entries, but the static access point doesn't see them.

At the point where I call CreateLogger, I have run the following identity checks:

var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GetName().Name.Contains("Serilog")).Select(x => x.Location);
var inmem = AppDomain.CurrentDomain.GetAssemblies().First(x => x.FullName.Contains("InMemory")).GetHashCode();
var hash = InMemorySink.Instance.GetHashCode();

When I run those same three checks at the location seen in the screenshot below, the results are as follows:

  • The assembles var shows the InMemory assembly location is the same in both places.
  • The inmem var shows that the hash code of the InMemory assembly is the same in both places.
  • The hash var shows that the static InMemorySink.Instance has indeed changed values!

I conclude from this that there is no funny business regarding mismatched DLL's or DLL versions. Instead it seems that internally, InMemory is somehow overwriting its static value. This seems like a bug.

Please help. :)

image

Random ArgumentException on Should extension method

I have a problem when making integration tests using InMemorySink.
An ArgumentException raises randomly when we execute "Should()" extension method over InMemorySink.

Error Message:
    System.ArgumentException : Destination array was not long enough. Check the destination index, length, and the array's lower bounds. (Parameter 'destinationArray')
   Stack Trace:
      at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
    at System.Collections.Generic.List`1.CopyTo(T[] array, Int32 arrayIndex)
    at System.Collections.ObjectModel.ReadOnlyCollection`1.CopyTo(T[] array, Int32 index)
    at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
    at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
    at Serilog.Sinks.InMemory.Assertions.InMemorySinkAssertions.SnapshotOf(InMemorySink instance)
    at Serilog.Sinks.InMemory.Assertions.InMemorySinkAssertions..ctor(InMemorySink instance)
    at Serilog.Sinks.InMemory.Assertions.InMemorySinkAssertionExtensions.Should(InMemorySink instance)
    at api_transferencias_empresas.Test.Unit.MetadataLoggerTestExtensions.VerifyLoggedOnceWithMetadata(InMemorySink logResults, LogEventLevel level, EventId eventId, String message) in /builds/desarrollo/productos-transaccionales/api-transferencias-empresas/Test/api_transferencias_empresas.Test.Unit/MetadataLoggerTestExtensions.cs:line 52

This is how we use it:
image

Am I doing something wrong?
Thanks you

Property chaining

Actual

var a = ... .Appearing()
                .Once()
                .WithLevel(LogEventLevel.Error);
a 
                .WithProperty()
                .WithValue();

a 
                .WithProperty()
                .WithValue();

Expected:

a
                .WithProperty()
                .WithValue()
                .And
                .WithProperty()
                .WithValue();;

Setting the output message formatter

Am I right in thinking the InMemorySink does not provide an option to set the outputFormatter? Because of the async nature of the InMemorySink.Instance I have had to register an instance of a singleton in my DI container. I want to include the Exception message in the RenderMessage but can't figure out how to do this.

This is the code I have

InMemorySink logSink = new();
services.AddSingleton(logSink);

_ = services.AddLogging(builder =>
{
    var logger = new LoggerConfiguration()
        .MinimumLevel.Information()
        .WriteTo.Sink(logSink, Serilog.Events.LogEventLevel.Information)
        .CreateLogger();

    builder.AddSerilog(logger);
});

A ConsoleSink for example takes a formatter as part of its constructor, however the same does not existing for the InMemorySink.

Is there any way to set the message formatter. It also appears that the extension method does nothing with the formatter?
https://github.com/serilog-contrib/SerilogSinksInMemory/blob/master/src/Serilog.Sinks.InMemory/InMemorySinkExtensions.cs

Updating to Fluent Assertions 6.0

Recently updated a repository to Fluent Assertions 6.0 and was running into some issues with the InMemoryAssertions:

 Message: 
    System.MissingMethodException : Method not found: 'Void FluentAssertions.Primitives.ReferenceTypeAssertions`2..ctor()'.

  Stack Trace: 
    InMemorySinkAssertions.ctor(InMemorySink instance)
    InMemorySinkAssertionExtensions.Should(InMemorySink instance)
    CreateFromContractMessageHandlerTests.ShouldLogIfInvalidAccountName(Mock`1 s3Client) line 344
    --- End of stack trace from previous location ---

When making a call like so:

InMemorySink.Instance.Should()
    .HaveErrorMessageWithException<ArgumentException>(
        "An invalid argument was encountered while processing message with ID {MessageId}",
        "Account name cannot be longer than 100 characters");

This is what the error message extension method looks like:

      public static void HaveErrorMessageWithException<T>(this InMemorySinkAssertions assertion, string messageTemplate, string innerMessage = null)
      {
          if (innerMessage == null)
          {
              assertion.HaveMessage(messageTemplate)
                  .WithLevel(LogEventLevel.Error)
                  .Appearing().Once()
                  .Match(o => o.Exception.GetType() == typeof(T));
          }

          assertion.HaveMessage(messageTemplate)
              .WithLevel(LogEventLevel.Error)
              .Appearing().Once()
              .Match(o => o.Exception.GetType() == typeof(T) &&
                      o.Exception.Message.Contains(innerMessage)
              );
      }

This was working with Fluent Assertions 5.10.3. Let me know if you need any more information

Missing enriched properties.

Memory sink is great for testing and I was trying it out with ASP.NET Core 5 but I am not able to find enriched properties.
How do I get enriched properties? I was trying to verify if the log has correlationid passed via the header.

I have setup logging in the test as

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Error()
                .WriteTo.InMemory()
                .WriteTo.File(new CompactJsonFormatter(), @"c:\temp\foo.log")
                .CreateLogger();

Then make a http call using the WebApplicationFactory fixture.

My startup.cs has httpcontext enriched.

            app.UseSerilogRequestLogging(options =>
            {
                options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
                {
                    diagnosticContext.Set("CorrelationId", GetCorrelationId(httpContext));
                };
            });

The text log has the correlation ID in it like
"CorrelationId":["something"]
but I am not able to verify it using InMemory test.

.NET Framework Support

Any chance of updating the projects with .NET Framework 4.5 targets? Would love to use this sink for unit testing, but only targeting .NET Standard is quite limiting.

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.