{"id":37028083,"url":"https://github.com/kwahome/sopa-api","last_synced_at":"2026-01-14T03:21:00.813Z","repository":{"id":57733887,"uuid":"158229081","full_name":"kwahome/sopa-api","owner":"kwahome","description":"Configurable and extensible structured logging on top of `slf4j` API to generate easily parsable log messages in desired formats.","archived":false,"fork":false,"pushed_at":"2023-11-22T06:19:12.000Z","size":144,"stargazers_count":1,"open_issues_count":5,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-11-15T09:49:34.923Z","etag":null,"topics":["android","gradle","java","java10","kotlin","maven","structured-logging"],"latest_commit_sha":null,"homepage":"https://kwahome.github.io/","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kwahome.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-19T13:37:13.000Z","updated_at":"2021-10-31T23:21:37.000Z","dependencies_parsed_at":"2022-08-24T11:20:31.174Z","dependency_job_id":null,"html_url":"https://github.com/kwahome/sopa-api","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/kwahome/sopa-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwahome%2Fsopa-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwahome%2Fsopa-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwahome%2Fsopa-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwahome%2Fsopa-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kwahome","download_url":"https://codeload.github.com/kwahome/sopa-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kwahome%2Fsopa-api/sbom","scorecard":{"id":574638,"data":{"date":"2025-08-11","repo":{"name":"github.com/kwahome/sopa-api","commit":"829b3e85bf969e742030c36a0aaf51b9518590e3"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.6,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":2,"reason":"Found 1/4 approved changesets -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":9,"reason":"binaries present in source code","details":["Warn: binary detected: gradle/wrapper/gradle-wrapper.jar:1"],"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-20T17:19:33.591Z","repository_id":57733887,"created_at":"2025-08-20T17:19:33.592Z","updated_at":"2025-08-20T17:19:33.592Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408824,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","gradle","java","java10","kotlin","maven","structured-logging"],"created_at":"2026-01-14T03:21:00.041Z","updated_at":"2026-01-14T03:21:00.805Z","avatar_url":"https://github.com/kwahome.png","language":"Java","readme":"# sopa-api\n[![Codacy Badge](https://api.codacy.com/project/badge/Grade/21b905dc40e542bfbd8477dcc9b0a7ca)](https://app.codacy.com/app/kwahome/sopa-api?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=kwahome/sopa-api\u0026utm_campaign=Badge_Grade_Dashboard)\n[![Build Status](https://travis-ci.com/kwahome/sopa-api.svg?branch=master)](https://travis-ci.com/kwahome/sopa-api)\n[![codecov](https://codecov.io/gh/kwahome/sopa-api/branch/master/graph/badge.svg)](https://codecov.io/gh/kwahome/sopa-api)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.kwahome.sopa/sopa-api.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.kwahome.sopa%22%20AND%20a:%22sopa-api%22)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n\u003e `Sopa` is hello in Maasai to which you reply `Sopa Oleng` 😀\n\nA configurable, extensible structured logging on top of `slf4j` API that generates easily parsable log messages in desired formats.\n\nLike `slf4j`, on top of whose API it's built, `sopa` is not an actual logging implementation. \nPlugging in your desired logging framework is thus still needed (e.g. java.util.logging, logback, log4j) at deployment time.\n\nIt started out as a logging util to add structure to logs in a project at my place of work but I now find myself copying it into all other projects. \nI'm tired of doing it. Now it's a library. ☺️\n\nYou are probably wondering why I just couldn't use existing libraries rather than falling into the craze and trope of writing one; and you're probably right in your wondersome thoughts. \nI did too but decided against it like would any other self-appointed benevolent dictator for life who's just starting out so here we are.\n\nMy short, not so convincing, poorly put together reason has something to do with being lazy; the good lazy. You see, when tackling the structured logging problem, \nI came across a number of good libraries (built on top of `slf4j` like I wanted) that I must admit I liked eventually borrowing a lot from. \n\nHowever, attributable to my laziness, I craved a set of extra features to allow me dump what I needed in my logs with minimal lines of code and zero repetition; \nchief amongst them being the ability to `bind` context to my logger which I would have otherwise had to copy over on every log line.\n\n## Change Log\nFor a changelog(release notes), see: https://github.com/kwahome/sopa-api/releases\n\n## Overview\nLog files are to developers what `Mjöllnir` is to `Thor`. \nThey're more often than not the last bastion of hope in figuring out what's what when the devil is in the details in your applications; \nwhich from my very little experience is almost every other time (whether running in production, performing UATs in a staging environment or in your development environment).\n\nThe problem with log files is that they are unstructured text data which makes it hard to query against for any sort of information or perform any useful analytics. \nThe goal of structured logging is to bring a more defined format and details to your logging for log files to be machine readable.\n\nStandard logging libraries like Simple Logging Facade for Java (SLF4J) already include a lot of useful information: timestamp, pid, thread, level, loggername, etc. \nWe just need to extend this list with attributes specific to our applications.\n\nStandard Java log messages look something like this:\n\n```java\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.Logger;\n\npublic class MyClass {\n    private static final Logger logger = LoggerFactory.getLogger(MyClass.name);\n    \n    public void myMethod() {\n        logger.debug(\"On the eighth day, God started debugging and he still hasn't finished\");\n    }\n}\n```\n\n```\n2018-01-27 16:17:58 DEBUG 90413 --- [nio-8080-exec-8] my.package.MyClass  : On the eighth day, God started debugging and he still hasn't finished yet\n```\n\nWhile it's human readable, it's rather quite difficult to parse by code in a log aggregation service, as it is unstructured text.\n\nIn it's place, a `key=value` formatted log message like one below is structured and much more friendly to a log aggregation service:\n\n```\n2018-01-27 16:17:58 DEBUG 90413 --- [nio-8080-exec-8] my.package.MyClass  : God started debugging, day=\"eight\", status=\"not finished\", bugsFound=\"7 billion\"\n```\n\nor in JSON format:\n\n```json\n{\n    \"message\": \"God started debugging!\",\n    \"status\": \"not finished\", \n    \"bugsFound\": \"7 billion\"\n}\n```\n\nor in YAML format:\n\n```yaml\nmessage: \"God started debugging!\",\nstatus: \"not finished\", \nbugsFound: \"7 billion\"\n```\n\n## Adding `sopa` to your build\n\n#### Gradle\n\n```groovy\ndependencies {\n  compile group: 'io.github.kwahome.sopa', name: 'sopa-api', version: '0.5.0'\n}\n```\n#### Maven\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003eio.github.kwahome.sopa\u003c/groupId\u003e\n  \u003cartifactId\u003esopa-api\u003c/artifactId\u003e\n  \u003cversion\u003e0.5.0\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## Usage\nIn place of the standard `slf4j` Logger, to use `sopa` you must instantiate it's Logger as illustrated below:\n\n```java\nimport io.github.kwahome.sopa.LoggerFactory;\nimport io.github.kwahome.sopa.Logger;\n\npublic class MyClass {\n    // instantiating a Logger\n    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);\n}\n```\n\nThe `Logger` interface offers the same `slf4j` logging APIs and an additional ones for use in binding context to a logger:\n\n```java\npublic interface Logger {\n    void error(String message, Object...params);\n    void warn(String message, Object...params);\n    void info(String message, Object...params);\n    void debug(String message, Object...params);\n    void trace(String message, Object...params);\n    \n    void newBind(Object...params);\n    void bind(Object...params);\n    void unbind(Object...params);\n}\n```\n### Configuration\n`sopa` is a PnP lib requiring no configuration to get started because it ships with defaults. \nIt defaults to a `KeyValueRenderer` to format log messages to the standard `key=value` comma separated pairs.\n\nHowever, it's also built to be Bring Your Own ... compliant thus has the `StructLoggerConfig` class with static methods to override default behaviour.\n\nBelow is an enumeration of configurable properties and how to go about making them fit your wants:\n\n\u003e It's advisable to make global configurations in the main thread of your application to avoid any concurrency issues.\n\n##### a) Default Log Renderer\nLog renderer is an instance of a class implementing the `LogRenderer` interface that is used in adding structure to your log messages. \nIt establishes the format (structure) you desire in your logs.\n\n`KeyValueRenderer` is set as the  default log renderer formatting log messages to `key=value` pairs.\n\nYou can configure a renderer of choice via the `setLogRenderer` setter with the only requirement being that the renderer implements the `LogRenderer` interface; e.g.\n\nTo configure the `JSONRenderer` (or `YAMLRenderer` which `sopa` ships with) as the preferred log renderer:\n\n```java\nimport io.github.kwahome.sopa.renderers.JSONRenderer;\nimport io.github.kwahome.sopa.StructLoggerConfig;\n\n/**\n * Main application class.\n */\n@SpringBootApplication\npublic class MyApplication {\n\n    public static void main(String[] args) {\n        StructLoggerConfig.setLogRenderer(JSONRenderer.getInstance()); // applies for any other renderers implementing LogRenderer\n        \n        SpringApplication.run(MyApplication.class, args);\n    }\n}\n```\n##### b) Default Value Renderer\nValue renderer is a function that accepts one argument \u0026 produces a result and that formats any object passed in as a value to any key-value entry. e.g:\n`(value) -\u003e value == null ? \"null\" : value.toString();` (which is the default value renderer) returns a `toString()`, regardless of object type unless it's null.\n\nTo configure a `valueRenderer` of choice:\n\n```java\nimport io.github.kwahome.sopa.StructLoggerConfig;\n\n/**\n * Main application class.\n */\n@SpringBootApplication\npublic class MyApplication {\n\n    public static void main(String[] args) {\n        Function\u003cObject, String\u003e myValueRenderer = (value) -\u003e value;\n        StructLoggerConfig.setValueRenderer(myValueRenderer);\n        \n        SpringApplication.run(MyApplication.class, args);\n    }\n}\n```\n\n##### c) Global Context Supplier\nThis is the supplier of application specific key-value pairs that are desired on every log entry (e.g. `host`, `environment`) and that should be bound to the logger class once.\n\nThe context supplier is defined as an optional loggable object `Optional\u003cLoggableObject\u003e` and initialized as an empty optional.\n\nTo configure your `contextSupplier` use `setContextSupplier`:\n\n```java\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\n\nimport io.github.kwahome.sopa.StructLoggerConfig;\n\n/**\n * Main application class.\n */\n@SpringBootApplication\npublic class MyApplication {\n    \n    private getEnvironment() {\n        return System.getenv().get(\"ENVIRONMENT\");\n    }\n    \n    private static InetAddress getHost() {\n        InetAddress inetAddress;\n        try {\n            inetAddress = InetAddress.getLocalHost();\n        } catch (UnknownHostException e) {\n            inetAddress = null;\n        }\n        return inetAddress;\n    }\n\n    public static void main(String[] args) {\n        StructLoggerConfig.setContextSupplier(\"environment\", getEnvironment(), \"host\", getHost());\n        \n        SpringApplication.run(MyApplication.class, args);\n    }\n}\n```\n\n`setContextSupplier` setter method is overloaded to also accept a `Map\u003cString, Object\u003e` or a POJO implementing the `LoggableObject` interface.\nThere's a detailed explanation on their usage(s) in the section about logging not too far below, so please continue reading 😊\n\n##### d) Log Entries Separator\nFor visual readability, a comma (`,`) is appended between key=value entries in a log message as a default behaviour.\n`,` is defined as the `logEntriesSeparator` configuration that can be changed if so wished.\n\nThe `logEntriesSeparator` shall only apply for the `KeyValueRenderer` as other renders such as the `JSONRenderer` will separate entries to their specification.\n\nTo configure the preferred `logEntriesSeparator` use `setLogEntriesSeparator`:\n\n```java\nimport io.github.kwahome.sopa.StructLoggerConfig;\n\n/**\n * Main application class.\n */\n@SpringBootApplication\npublic class MyApplication {\n\n    public static void main(String[] args) {\n        StructLoggerConfig.setLogEntriesSeparator(\";\");\n        \n        SpringApplication.run(MyApplication.class, args);\n    }\n}\n```\n\n### Logging key-value pairs\n\n##### a) Object array of [\"key\", \"value'] pairs\nThe APIs exposed for logging at different levels take in a string message \u0026 an array of object params which is the building block of passing in key=value pairs to the logger.\n\nPass in key-value pairs as object array params following the convention key on the left, value on the right \n(i.e. `[\"key1\", \"value1\", \"key2\", \"value2\"]`) which should yield pairs `key1=value` and `key2=value2`; \nthus keys are on even indices, values on odd indices\n\nAll keys must be strings, but the values can be of any type, e.g:\n\n```java\npublic class MyClass {\n\n    public void myMethod() {\n        LOGGER.info(\"start\", \n                    \"user\", MyClass.getUser(),\n                    \"requestId\", MyClass.getRequestId());\n        \n        // Oh! The indentation is for readability \u0026 cognition 😎\n    }\n}\n```\n\nwhich would result in a log message:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew\n```\n\nSince the key-value pairs passed in as array items with the key and value at alternative indices, \nI must admit it can get out of hand and be a bit confusing because an odd number of items in the array would indicate a dangling key-value pair. \nBut hey, there are no named arguments in most strongly typed languages like `java` so we can only make the most out of what's available.\n\n##### b) `Map\u003cString, Object\u003e` objects\nFrom the above rant, I decided (read was persuaded) to add in support for `Map\u003cString, Object\u003e` objects that offer better `key=value` management.\n\nThus the example above would become:\n```java\npublic class MyClass {\n\n    public void myMethod() {\n        Map\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n        map.put(\"user\", MyClass.getUser());\n        map.put(\"requestId\", MyClass.getRequestId());\n        LOGGER.info(\"start\", map);\n        \n        // you can pass in any number of Map\u003cString, Object\u003e objects \n        // since it's all based on an array of objects and they'll all be logged\n    }\n}\n```\n\nwhich would result in a log message:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew\n```\n\nGranted you have more logging oriented lines of code, but you'll never miss a key=value pair or have to count through the items you're passing in. 😃 \n\n#####  c) Objects implementing the `LoggableObject` interface\nAny plain old java object in your code can implement the `LoggableObject` interface which allows you to make any class loggable, e.g:\n\n```java\npublic class MyClass implements LoggableObject {\n\n    private String userName;\n    private String requestId;\n    \n    public String getUserName() {\n        return userName;\n    }\n    \n    public String getRequestId() {\n        return requestId;\n    }\n\n    @Override\n    public Object[] loggableObject() {\n        // returns an object array with the treasured key-value pairs\n        // similar to toString() but for sopa key-value pairs\n        return new Object[]{\"userName\", getUserName(), \"requestId\", getRequestId()};\n    }\n}\n```\n\nThen you can just pass in the object instance directly, without the need to specify any key-value pairs, e.g:\n\n```java\npublic class MyClass {\n\n    public void myMethod() {\n        LOGGER.info(\"start\", new MyClass());\n        // you can pass in any number of such POJOs\n        // since it's all based on an array of objects and they'll all be logged\n    }\n}\n```\n\nwhich would result in a log message:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew\n```\n\n#####  d) Mixing up the alternatives\nThough not advisable (because of the confusion that will ensue many light years later when you are older, probably wiser \u0026 looking at your code with disgust), \nit is possible to pass in a mix of `[\"key\", \"value\"]` pairs, `Map\u003cString, Object\u003e` objects \u0026 `LoggableObject` objects in one call to the logging APIs.\n\nWhat's more is that `Map\u003cString, Object\u003e` or a `LoggableObject` object passed in as values to keys in a pair are not iterated over but rather logged as values of the respective keys.\n\nThe wizard of `sopa` handling it all beneath, without being overly presumptuous, is endowed with enough level of wit to discern and tell them apart.\n\nExample:\n```java\npublic class MyClass {\n\n    public void myMethod() {\n       LoggableObject loggableObject = new MyClass(); // using MyClass from above\n       \n       Map\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n       map.put(\"age\", 20);\n       map.put(\"gender\", \"female\");\n       \n       LOGGER.info(\"start\", map, \"myMap\", map, loggableObject, \"myLoggableObject\", loggableObject);\n    }\n}\n```\n\nwhich would result in a log message:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, age=20, gender=female, myMap=\"{age=20, gender=female}\", user=johndoe@gmail.com, requestId=xyz123dgew, loggableObject=my.package.MyClass@17c386de\n```\n\n### Logging Exceptions\nUnlike in `slf4j`, there is no separate API for use in logging a `Throwable`. \n\nInstead, just pass in the exception(s) as a parameter (order is not important) and it's root cause message will be extracted and logged under the key `errorMessage`. \nThe entire exception stack trace will also be appended to the log message.\n\nExample:\n```java\npublic class MyClass {\n\n    public void myMethod() {\n       try {\n           System.out.println();\n           // some terrorist code (read bugs) here\n           // it blows up 💥\n           // and now the world is not a safe place anymore \n           // but has it ever been? 😬\n           // you get the drift\n       } catch (Exception e) {\n           // but gladly it's all caught in the act\n           // so we decide to log it for the helpless mortal in a dark basement \n           // with a cup of coffee who's just about to buy a bar going like \"WTF is going on?\"\n           \n           LOGGER.error(\"oops!\",\n               \"user\", MyClass.getUser(),\n               \"requestId\", MyClass.getRequestId(),\n               e);\n           \n           // Oh again! The indentation is for readability \u0026 cognition 😎\n       }\n    }\n}\n```\n\nwhich would result in a log event like:\n\n```\n2018-01-27 16:17:58 ERROR 90413 --- [nio-8080-exec-8] my.package.MyClass  : oops!, user=johndoe@gmail.com, requestId=xyz123dgew, errorMessage=\"May the force be with you!\",\n...followed by the regular scary full stack trace of the exception...\n```\n\nIn the above example, the exception will still get logged even if the `Throwable` object was passed in in another position with only the order of entries in the log message being a wee bit different.\n\nThe same behaviour can be observed on all other logging level APIs but perhaps you'll most likely never log an exception on any other level apart from `error` because why would you?\n\n### Logger Context\nTo make logging less painful and more powerful, `sopa` allows you to bind, re-binding and unbind key-value pairs to your loggers to ensure they are present in every following logging call without having to repeat them over and over.\n\nTwo types of logger contexts exist:\n\n##### 1. Global Context\nThis is application specific key-value pairs that are desired on every log message (e.g. `host` or `environment`) and would usually be bound to the logger class once.\nThis is the context set using `setContextSupplier` as described in the configuration section earlier.\n\nExample (using a `Map`; the earlier example used `key-value` params):\n\n```java\nimport io.github.kwahome.sopa.StructLoggerConfig;\n\n/**\n * Main application class.\n */\n@SpringBootApplication\npublic class MyApplication {\n    \n    private getEnvironment() {\n        return System.getenv().get(\"ENVIRONMENT\");\n    }\n    \n    private static InetAddress getHost() {\n        InetAddress inetAddress;\n        try {\n            inetAddress = InetAddress.getLocalHost();\n        } catch (UnknownHostException e) {\n            inetAddress = null;\n        }\n        return inetAddress;\n    }\n\n    public static void main(String[] args) {\n        Map\u003cString, Object\u003e globalLoggerContext = new HashMap\u003c\u003e();\n        globalLoggerContext.put(\"environment\", getEnvironment());\n        globalLoggerContext.put(\"host\", getHost());\n        StructLoggerConfig.setContextSupplier(globalLoggerContext);\n        \n        SpringApplication.run(MyApplication.class, args);\n    }\n}\n```\n\nAny call to the logger will bear the set global context.\n\nIt's strongly advisable to do this in the mean thread.\n\n##### 2. Instance Bound Context\nSimilar to global context, `sopa` allows you to bind key-value pairs that appear on every log message generated by a `Logger` instance \nto avoid duplicating cross-cutting concerns on all calls to the logging APIs.\n\nThe `Logger` interface exposes **`newBind(Object...params)`**, **`bind(Object...params)`** and **`unbind(Object...params)`** methods that \naccept `[\"key\", \"value\"]` pairs, `Map\u003cString, Object\u003e` objects or `LoggableObject` objects for the purpose of binding \u0026 clearing logger context.\n\n\u003e **`newBind(Object...params)`** allows you to bind new context and overwrite any existing\n\n\u003e **`bind(Object...params)`** allows you to update bound context\n\n\u003e **`unbind(Object...params)`** allows you to remove key-values from bound context\n\nExamples:\n\na) **`newBind()`**\n\n```java\n\npublic class MyClass {\n    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);\n    \n    public void myMethod() {\n        Map\u003cString, Object\u003e context = new HashMap\u003c\u003e();\n        context.put(\"user\", MyClass.getUser());\n        context.put(\"requestId\", MyClass.getRequestId());\n        LOGGER.newBind(context); // you can pass in key-value pairs in an array or a LoggableObject. Or a mix of those options\n        // some code that does something extra-ordinary goes here\n        // some more code (or bugs)\n        LOGGER.info(\"start\");\n        // some other code\n        // and more where that came from\n        LOGGER.info(\"end\");\n    }\n}\n```\nwhich would result in log messages:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : end, user=johndoe@gmail.com, requestId=xyz123dgew\n```\n\nb) **`bind()`**\n\n```java\npublic class MyClass {\n    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);\n    \n    public void myMethod() {\n        Map\u003cString, Object\u003e context = new HashMap\u003c\u003e();\n        context.put(\"user\", getUser());\n        context.put(\"requestId\", getRequestId());\n        LOGGER.newBind(context); // you can pass in key-value pairs in an array or a LoggableObject. Or a mix of those options\n       // some code that does something extra-ordinary goes here\n        LOGGER.info(\"received\");\n        // some more code (or bugs)\n        // several quantum leaps of code later\n        // you hit some new info that is of significance\n        LOGGER.bind(\"age\", getAge(), \"gender\", getGender());\n        // some more code (or bugs)\n        LOGGER.info(\"start\");\n        // some other code\n        // and more where that came from\n        LOGGER.info(\"end\");\n    }\n}\n```\nwhich would result in log messages:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : received, user=johndoe@gmail.com, requestId=xyz123dgew\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew, age=20, gender=Female\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : end, user=johndoe@gmail.com, requestId=xyz123dgew, age=20, gender=Female\n```\n\nc) **`unbind()`**\n\n```java\npublic class MyClass {\n    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);\n    \n    public void myMethod() {\n        Map\u003cString, Object\u003e context = new HashMap\u003c\u003e();\n        context.put(\"user\", getUser());\n        context.put(\"requestId\", getRequestId());\n        LOGGER.newBind(context); // you can pass in key-value pairs in an array or a LoggableObject. Or a mix of those options\n        // some code that does something extra-ordinary goes here\n        LOGGER.info(\"received\");\n        // several quantum leaps of code later\n        // you hit some new info that is of significance\n        LOGGER.bind(\"age\", getAge(), \"gender\", getGender());\n        // some more code (or bugs)\n        LOGGER.info(\"start\");\n        // some other code that you don't remember putting in\n        // but you decide you don't need some of the bound info in logs from this section\n        LOGGER.unbind(\"age\", getAge());\n        LOGGER.info(\"end\");\n    }\n}\n```\n\nwhich would result in log messages:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : received, user=johndoe@gmail.com, requestId=xyz123dgew\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, user=johndoe@gmail.com, requestId=xyz123dgew, age=20, gender=Female\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : end, user=johndoe@gmail.com, requestId=xyz123dgew, gender=Female\n```\n\n### Helper Utils\n`sopa` has a static class `Helpers` in the utils with methods useful in converting a `Map\u003cString, Object\u003e` into an `Object[]` and the converse.\nThey are used internally in converting passed in objects back and forth which is the clever trick behind supporting logging params alternatives.\n\nThey can be helpful to you as well especially while implementing the `LoggableObject` in your class. e.g:\n\n`Car.java`\n\n```java\nimport io.kwahome.github.sopa.interfaces.LoggableObject;\nimport io.kwahome.github.sopa.utils.Helpers;\n\n\npublic class Car implements LoggableObject {\n    private String make;\n    private String model;\n    private String engineCapacity;\n    \n    Car(String make, String model, int capacity) {\n        this.make = make;\n        this.model = model;\n        this.capacity = capacity;\n    }\n    \n    public String getMake() {\n        return make;\n    }\n    \n    public String getModel() {\n        return model;\n    }\n    \n    public int getEngineCapacity() {\n        return engineCapacity;\n    }\n    \n    @Override\n    public Object[] loggableObject() {\n        /*\n        * rather than:\n        * \n        * return Object[]{\"make\", getMake(), \n        *                 \"model\", getModel(), \n        *                 \"engineCapacity\", getEngineCapacity()}\n        *                 \n        * use a map and convert it to Object[]            \n        * */\n        Map\u003cString, Object\u003e carLoggableContext = new HashMap\u003c\u003e();\n        carLoggableContext.put(\"make\", getMake());\n        carLoggableContext.put(\"model\", getModel());\n        carLoggableContext.put(\"engineCapacity\", getEngineCapacity());\n        \n        return Helpers.mapToObjectArray(carLoggableContext);\n    }\n}\n```\n\n`MyClass.java`\n```java\nimport java.util.Date;\n\n\npublic class MyClass {\n    private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);\n    \n    private Car myCar = new Car(\"mercedes\", \"s-65\", \"2000cc\");\n    \n    public void myMethod() {\n        LOGGER.bind(myCar); // bind with myCar loggable object\n        \n        LOGGER.info(\"start\", \"time\", new Date());\n        // ...\n        // some code to keep the engine running\n        // ...\n        LOGGER.info(\"stop\", \"time\", new Date());\n    }\n}\n```\n\nwhich would result in log messages:\n\n```\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : start, make=mercedes, model=s-65, capacity=200cc, time=\"Sun Nov 25 09:43:17 EAT 2018\"\n\n2018-01-27 16:17:58 INFO 90413 --- [nio-8080-exec-8] my.package.MyClass  : stop, make=mercedes, model=s-65, capacity=200cc, time=\"Sun Nov 25 11:43:17 EAT 2018\"\n``` \n\n## Contributing\nPlease read [CONTRIBUTING.md](https://github.com/kwahome/sopa-api/blob/master/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](https://github.com/kwahome/sopa-api/blob/master/CODE_OF_CONDUCT.md) for details on our code of conduct, and the process for submitting pull requests to us.\n\n## Versioning\nWe use [SemVer](https://semver.org/) for versioning. For the versions available, see the tags on this repository.\n\n## License\nThis software is licensed under the MIT License. See the [LICENSE](https://github.com/kwahome/sopa-api/blob/master/LICENSE) file in the top distribution directory for the full license text.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkwahome%2Fsopa-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkwahome%2Fsopa-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkwahome%2Fsopa-api/lists"}