├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── new-language-proposal.md │ └── user-story.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── cpp-tests.yaml │ ├── go-tests.yaml │ ├── python-tests.yml │ ├── release.yml │ └── ts-tests.yaml ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ci └── release.sh ├── docs ├── adr │ ├── 000-template.md │ └── 001-releases.md └── implementation-spec.md ├── renovate.json └── src ├── cpp ├── .dockerignore ├── .gitignore ├── CMakeLists.txt ├── Dockerfile ├── README.md ├── ci │ └── develop.sh ├── cmake │ └── CPM.cmake ├── include │ └── alog │ │ └── logger.hpp ├── src │ └── logger.cpp ├── tests │ ├── include │ │ └── unit_testing.h │ ├── logger_test.cpp │ └── src │ │ └── main.cpp └── tools │ ├── alog_fib_example │ ├── .gitignore │ ├── CMakeLists.txt │ ├── README.md │ ├── cmake │ │ └── CPM.cmake │ ├── include │ │ ├── fibonacci.h │ │ └── util.h │ ├── main.cpp │ └── src │ │ ├── fibonacci.cpp │ │ └── util.cpp │ └── list_channels │ └── list_channels.sh ├── go ├── .gitignore ├── Dockerfile ├── README.md ├── alog │ ├── alog.go │ ├── alog_extras.go │ ├── alog_extras_test.go │ ├── alog_test.go │ └── helpers_test.go ├── bin │ ├── alog_json_converter │ │ ├── .gitignore │ │ └── main.go │ ├── go.mod │ └── go.sum ├── ci │ ├── develop.sh │ └── test_release.sh ├── example │ ├── alog_example_server │ │ ├── .gitignore │ │ ├── main.go │ │ └── test_example_server.sh │ ├── go.mod │ └── go.sum ├── go.mod └── go.sum ├── python ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── __init__.py ├── alog │ ├── __init__.py │ ├── alog.py │ └── protocols.py ├── ci │ ├── develop.sh │ ├── publish.sh │ └── run-tests.sh ├── requirements_test.txt ├── setup.py ├── tests │ ├── fixtures │ │ ├── duplicate_replacement │ │ │ ├── test_case_1.py │ │ │ └── test_case_2.py │ │ └── placeholders │ │ │ ├── test_case_3.py │ │ │ └── test_case_4.py │ ├── test_alog.py │ ├── test_alog_clean_import.py │ ├── test_protocols.py │ └── test_util.py └── util │ ├── alog_json_converter.py │ ├── correct_log_codes.py │ └── requirements.txt └── ts ├── .dockerignore ├── .gitignore ├── .npmignore ├── Dockerfile ├── README.md ├── TODO.md ├── ci ├── develop.sh └── publish.sh ├── package-lock.json ├── package.json ├── src ├── alog.ts ├── channel-log.ts ├── configure.ts ├── core.ts ├── formatters.ts ├── index.ts └── types.ts ├── test ├── api_test.ts ├── helpers.ts └── internals_test.ts ├── tsconfig.json └── tslint.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | gabe.l.hart@gmail.com 2 | ghart@us.ibm.com 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## Platform 14 | 15 | - [ ] `python` 16 | * Interpreter version: 17 | * Library version: 18 | - [ ] `typescript` 19 | * `node` version: 20 | * Package version: 21 | - [ ] `go` 22 | * Operating system: 23 | * Compiler version: 24 | * Package version: 25 | - [ ] `c++` 26 | * Operating system: 27 | * Compiler version: 28 | * Library version: 29 | * Project build system: 30 | 31 | 32 | ## Sample Code 33 | Please include a minimal sample of the code that will (if possible) reproduce the bug in isolation 34 | 35 | ## Expected behavior 36 | A clear and concise description of what you expected to happen. 37 | 38 | ## Observed behavior 39 | What you see happening (error messages, stack traces, etc...) 40 | 41 | ## Additional context 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-language-proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New language proposal 3 | about: Propose adding an implementation of Alchemy Logging in a new language 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Use Case Overview 11 | 12 | Please describe which language you are proposing support for, and the use case for the project where you would like to use Alchemy Logging. 13 | 14 | ## API Overview 15 | 16 | In this section, please outline how a user will leverage the features of the [Implementation Spec](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#1-interface). 17 | 18 | ### Required Features 19 | 20 | * **Core Singleton**: 21 | * **Channels** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#channels-and-levels)): 22 | * **Levels** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#channels-and-levels)): 23 | * **Single-call global configuration** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#11-configuration)): 24 | * **Formatting**: 25 | * **Pretty-print formatting** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#14-pretty-formatting)): 26 | * **Json formatting** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#15-json-formatting)): 27 | * **Usage**: 28 | * **Individual Records** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#131-individual-text-records)): 29 | * **Shared Channels** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#133-channel-log-objects-and-functions)): 30 | 31 | ### Optional Features 32 | 33 | * **Per-message map data** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#132-map-records)): 34 | * **Thread IDs** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#11-configuration)): 35 | * **Thread log function** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#161-thread-logs)): 36 | * **Metadata** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#162-metadata)): 37 | * **Log scopes** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#163-scoped-logs)): 38 | * **Function scopes** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#164-function-trace-scopes)): 39 | * **Timed scopes** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#165-timed-scopes)): 40 | 41 | ## Implementation Details 42 | 43 | In this section, please outline how you plan to implement the core elements of the [Implementation Spec](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#2-implementation). 44 | 45 | ### Required Implementaiton Details 46 | 47 | * **Core Singleton** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#21-core)): 48 | * **Lazy Message Construction** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#22-lazy-message-construction)): 49 | * **Thread Safety** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#23-thread-safety)) 50 | 51 | ### Optional Implementation Details 52 | 53 | * **Dynamic Configureation** ([reference](https://github.com/IBM/alchemy-logging/blob/main/docs/implementation-spec.md#24-dynamic-configuration)) 54 | 55 | ### Dependencies 56 | 57 | Please indicate any/all non-default dependencies that this implementation will need. 58 | 59 | ## Alternatives 60 | 61 | In this section, please provide details about alternate approaches that could be taken and why the proposed approach is the right choice. 62 | 63 | ### Other Common Logging Packages 64 | 65 | List the most commonly used logging packages in the target language and how they differ from `Alchemy Logging`. 66 | 67 | ### Open Questions 68 | 69 | List out any open questions you'd like to work through in the course of the proposal. 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/user-story.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: User story 3 | about: A user-oriented story describing a piece of work to do 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | As a , I want to , so that I can 13 | 14 | ## Discussion 15 | 16 | Provide detailed discussion here 17 | 18 | ## Acceptance Criteria 19 | 20 | 21 | - [ ] Unit tests cover new/changed code 22 | - [ ] Examples build against new/changed code 23 | - [ ] READMEs are updated 24 | - [ ] Type of [semantic version](https://semver.org/) change is identified 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "pip" 15 | directory: "/src/python/" 16 | schedule: 17 | interval: "daily" 18 | 19 | - package-ecosystem: "npm" 20 | directory: "/src/ts/" 21 | schedule: 22 | interval: "daily" 23 | 24 | - package-ecosystem: "gomod" 25 | directory: "/src/go/" 26 | schedule: 27 | interval: "daily" 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | What does this PR do? 4 | 5 | ## Changes 6 | 7 | What changes are included in this PR? 8 | 9 | ## Testing 10 | 11 | How did you test this PR? 12 | 13 | ## Related Issue(s) 14 | 15 | List any issues related to this PR 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '17 2 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'cpp', 'go', 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 https://git.io/JvXDl 55 | 56 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 57 | # and modify them (or add more) to build your code if your project 58 | # uses a compiled language 59 | 60 | - name: build-cpp 61 | if: matrix.language == 'cpp' 62 | working-directory: ./src/cpp 63 | run: | 64 | mkdir build 65 | cd build 66 | cmake .. -D BUILD_UNIT_TESTS=OFF 67 | make 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/cpp-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs the c++ implementation unit tests 2 | name: cpp-tests 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: {} 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Run unit tests 15 | run: | 16 | cd src/cpp 17 | docker build . --target=test 18 | -------------------------------------------------------------------------------- /.github/workflows/go-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs the go implementation unit tests 2 | name: go-tests 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: {} 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Run unit tests 15 | run: | 16 | cd src/go 17 | docker build . --target=test 18 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs the typescript implementation unit tests 2 | name: python-tests 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: {} 9 | jobs: 10 | build-36: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Run unit tests 15 | run: | 16 | cd src/python 17 | docker build . --target=test --build-arg PYTHON_VERSION=3.6 18 | build-37: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Run unit tests 23 | run: | 24 | cd src/python 25 | docker build . --target=test --build-arg PYTHON_VERSION=3.7 26 | build-38: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Run unit tests 31 | run: | 32 | cd src/python 33 | docker build . --target=test --build-arg PYTHON_VERSION=3.8 34 | build-39: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Run unit tests 39 | run: | 40 | cd src/python 41 | docker build . --target=test --build-arg PYTHON_VERSION=3.9 42 | build-310: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Run unit tests 47 | run: | 48 | cd src/python 49 | docker build . --target=test --build-arg PYTHON_VERSION=3.10 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs on any release 2 | name: release 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Release script 13 | run: REF="${{ github.ref }}" ./ci/release.sh 14 | env: 15 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 16 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ts-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs the typescript implementation unit tests 2 | name: ts-tests 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: {} 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Run unit tests 15 | run: | 16 | cd src/ts 17 | docker build . --target=test 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This CMakeLists file simply aliases to the c++ implementation so that the repo 2 | # can be used directly via CPM without the need for a SOURCE_SUBDIR 3 | cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) 4 | project(alog_wrapper) 5 | add_subdirectory(./src/cpp) 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our 12 | community include: 13 | 14 | - Demonstrating empathy and kindness toward other people 15 | - Being respectful of differing opinions, viewpoints, and experiences 16 | - Giving and gracefully accepting constructive feedback 17 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 18 | - Focusing on what is best not just for us as individuals, but for the overall community 19 | 20 | Examples of unacceptable behavior include: 21 | 22 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 23 | - Trolling, insulting or derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or email address, without their explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Enforcement Responsibilities 29 | 30 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 31 | 32 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 33 | 34 | ## Scope 35 | 36 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. 37 | 38 | ## Enforcement 39 | 40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement - [CODOWNERS](.github/CODEOWNERS.md). All complaints will be reviewed and investigated promptly and fairly. 41 | 42 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 43 | 44 | ## Enforcement Guidelines 45 | 46 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 47 | 48 | ### 1. Correction 49 | 50 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 51 | 52 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 53 | 54 | ### 2. Warning 55 | 56 | **Community Impact**: A violation through a single incident or series of actions. 57 | 58 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 59 | 60 | ### 3. Temporary Ban 61 | 62 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 63 | 64 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 65 | 66 | ### 4. Permanent Ban 67 | 68 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 69 | 70 | **Consequence**: A permanent ban from any sort of public interaction within the community. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 75 | 76 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 77 | 78 | [homepage]: https://www.contributor-covenant.org 79 | 80 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👍🎉 First off, thank you for taking the time to contribute! 🎉👍 4 | 5 | The following is a set of guidelines for contributing. These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | - [What Should I Know Before I Get Started?](#what-should-i-know-before-i-get-started) 8 | - [Code of Conduct](#code-of-conduct) 9 | - [How Do I Start Contributing?](#how-do-i-start-contributing) 10 | - [How Can I Contribute?](#how-can-i-contribute) 11 | - [Reporting Bugs](#reporting-bugs) 12 | - [How Do I Submit A (Good) Bug Report?](#how-do-i-submit-a-good-bug-report) 13 | - [Suggesting Enhancements](#suggesting-enhancements) 14 | - [How Do I Submit A (Good) Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) 15 | - [Set up your dev environment](#set-up-your-dev-environment) 16 | - [Docker Setup (fast)](#docker-setup-fast) 17 | - [Native Setup (Advanced)](#native-setup-advanced) 18 | - [Managing dependencies](#managing-dependencies) 19 | - [Code formatting](#code-formatting) 20 | - [Useful commands](#useful-commands) 21 | - [Your First Code Contribution](#your-first-code-contribution) 22 | - [How to contribute](#how-to-contribute) 23 | - [Code Review](#code-review) 24 | - [Releasing (Maintainers only)](#releasing-maintainers-only)% 25 | 26 | ## What Should I Know Before I Get Started? 27 | 28 | If you're new to GitHub and working with open source repositories, this section will be helpful. Otherwise, you can skip to learning how to [set up your dev environment](#set-up-your-dev-environment) 29 | 30 | ### Code of Conduct 31 | 32 | This project adheres to the [Contributor Covenant](./CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 33 | 34 | Please report unacceptable behavior to one of the [Code Owners](./.github/CODEOWNERS). 35 | 36 | ### How Do I Start Contributing? 37 | 38 | The below workflow is designed to help you begin your first contribution journey. It will guide you through creating and picking up issues, working through them, having your work reviewed, and then merging. 39 | 40 | Help on inner source projects is always welcome and there is always something that can be improved. For example, documentation (like the text you are reading now) can always use improvement, code can always be clarified, variables or functions can always be renamed or commented on, and there is always a need for more test coverage. If you see something that you think should be fixed, take ownership! Here is how you get started: 41 | 42 | ## How Can I Contribute? 43 | 44 | When contributing, it's useful to start by looking at [issues](https://github.com/IBM/alchemy-logging/issues). After picking up an issue, writing code, or updating a document, make a pull request and your work will be reviewed and merged. If you're adding a new feature, it's best to [write an issue](https://github.com/IBM/alchemy-logging/issues/new?assignees=&labels=&template=feature_request.md&title=) first to discuss it with maintainers first. 45 | 46 | ### Reporting Bugs 47 | 48 | This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report ✏️, reproduce the behavior 💻, and find related reports 🔎. 49 | 50 | #### How Do I Submit A (Good) Bug Report? 51 | 52 | Bugs are tracked as [GitHub issues using the Bug Report template](https://github.com/IBM/alchemy-logging/issues/new?assignees=&labels=&template=bug_report.md&title=). Create an issue on that and provide the information suggested in the bug report issue template. 53 | 54 | ### Suggesting Enhancements 55 | 56 | This section guides you through submitting an enhancement suggestion, including completely new features, tools, and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion ✏️ and find related suggestions 🔎 57 | 58 | #### How Do I Submit A (Good) Enhancement Suggestion? 59 | 60 | Enhancement suggestions are tracked as [GitHub issues using the Feature Request template](https://github.com/IBM/alchemy-logging/issues/new?assignees=&labels=&template=feature_request.md&title=). Create an issue and provide the information suggested in the feature requests or user story issue template. 61 | 62 | #### How Do I Submit A (Good) Improvement Item? 63 | 64 | Improvements to existing functionality are tracked as [GitHub issues using the User Story template](https://github.com/IBM/alchemy-logging/issues/new?assignees=&labels=&template=user-story.md&title=). Create an issue and provide the information suggested in the feature requests or user story issue template. 65 | 66 | #### How Do I Propose Adding Support For A New Language? 67 | 68 | When proposing support for a new language, please submit a [GitHub issue using the New Language Proposal template](https://github.com/IBM/alchemy-logging/issues/new?assignees=&labels=&template=new-language-proposal.md&title=). 69 | 70 | ## Set up your dev environments 71 | 72 | Each language-specific implementation has its own dockerized development environment. You can launch the dev environment by running `ci/develop.sh`: 73 | 74 | ```sh 75 | cd src/ 76 | ./ci/develop.sh 77 | ``` 78 | 79 | For each language, this will mount the `src/` directory to `/src` in a running docker container that has all of the necessary dependencies available. From there, each implementation has its own language-specific development flow. 80 | 81 | ### Python 82 | 83 | ```sh 84 | # Run unit tests 85 | ./ci/run-tests.sh 86 | ``` 87 | 88 | ### Typescript 89 | 90 | ```sh 91 | # Compile typescript 92 | npm run build 93 | 94 | # Run unit tests 95 | npm run test 96 | ``` 97 | 98 | ### Go 99 | 100 | ```sh 101 | # Build the code 102 | go build ./... 103 | 104 | # Run unit tests 105 | go test -coverprofile coverage.html ./... 106 | ``` 107 | 108 | ### C++ 109 | 110 | ```sh 111 | # For the first time build, make the build directory, otherwise skip this 112 | mkdir build 113 | 114 | # Configure the build pointing at the mounted /src directory. This may take a 115 | # few minutes the first time to pull down dependencies 116 | cd build 117 | cmake /src 118 | 119 | # Build the dependencies and library code 120 | make -j$(nproc) 121 | 122 | # Run the unit tests 123 | make test 124 | ``` 125 | 126 | ## Your First Code Contribution 127 | 128 | Unsure where to begin contributing? You can start by looking through these issues: 129 | 130 | - Issues with the [`good first issue` label](https://github.com/IBM/alchemy-logging/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - these should only require a few lines of code and are good targets if you're just starting contributing. 131 | - Issues with the [`help wanted` label](https://github.com/IBM/alchemy-logging/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) - these range from simple to more complex, but are generally things we want but can't get to in a short time frame. 132 | 133 | ### How to contribute 134 | 135 | To contribute to this repo, you'll use the Fork and Pull model common in many open source repositories. For details on this process, watch [how to contribute](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 136 | 137 | When ready, you can create a pull request. Pull requests are often referred to as "PR". In general, we follow the standard [github pull request](https://help.github.com/en/articles/about-pull-requests) process. Follow the template to provide details about your pull request to the maintainers. 138 | 139 | Before sending pull requests, make sure your changes pass tests. 140 | 141 | #### Code Review 142 | 143 | Once you've [created a pull request](#how-to-contribute), maintainers will review your code and likely make suggestions to fix before merging. It will be easier for your pull request to receive reviews if you consider the criteria the reviewers follow while working. Remember to: 144 | 145 | - Run tests locally and ensure they pass 146 | - Follow the project coding conventions 147 | - Write detailed commit messages 148 | - Break large changes into a logical series of smaller patches, which are easy to understand individually and combine to solve a broader issue 149 | 150 | ## Releasing (Maintainers only) 151 | 152 | The responsibility for releasing new versions of the libraries falls to the maintainers. For details on the process, [see the ADR](docs/adr/001-releases.md). 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 International Business Machines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5788/badge)](https://bestpractices.coreinfrastructure.org/projects/5788) 2 | 3 | # Alchemy Logging (alog) 4 | The `alog` framework provides tunable logging with easy-to-use defaults and power-user capabilities. The mantra of `alog` is **"Log Early And Often"**. To accomplish this goal, `alog` makes it easy to enable verbose logging at develop/debug time and trim the verbosity at production run time, all while avoiding performance degradation by using lazily evaluated log messages which only render if enabled. The `alog` project maintains language-naitve implementations in many popular programming languages with the goal of giving a consistent application logging experience both when writing your code and when administering a cloud service written in multiple programming languages. 5 | 6 | ## Channels and Levels 7 | The primary components of the framework are **channels** and **levels** which allow for each log statement to be enabled or disabled when appropriate. 8 | 9 | 1. **Levels**: Each logging statement is made at a specific level. Levels provide sequential granularity, allowing detailed debugging statements to be placed in the code without clogging up the logs at runtime. 10 | 11 |
12 | 13 | The sequence of levels and their general usage is as follows: 14 | 15 | 16 | 1. `off`: Disable the given channel completely 17 | 1. `fatal`: A fatal error has occurred. Any behavior after this statement should be regarded as undefined. 18 | 1. `error`: An unrecoverable error has occurred. Any behavior after this statement should be regarded as undefined unless the error is explicitly handled. 19 | 1. `warning`: A recoverable error condition has come up that the service maintainer should be aware of. 20 | 1. `info`: High-level information that is valuable at runtime under moderate load. 21 | 1. `trace`: Used to log begin/end of functions for debugging code paths. 22 | 1. `debug`: High-level debugging statements such as function parameters. 23 | 1. `debug1`: High-level debugging statements. 24 | 1. `debug2`: Mid-level debugging statements such as computed values. 25 | 1. `debug3`: Low-level debugging statements such as computed values inside loops. 26 | 1. `debug4`: Ultra-low-level debugging statements such as data dumps and/or statements inside multiple nested loops. 27 | 28 |
29 | 30 | 1. **Channels**: Each logging statement is made to a specific channel. Channels are independent of one another and allow for logical grouping of log messages by functionality. A channel can be any string. A channel may have a specific **level** assigned to it, or it may use the configured default level if it is not given a specific level filter. 31 | 32 | Using this combination of **Channels** and **Levels**, you can fine-tune what log statements are enabled when you run your application under different circumstances. 33 | 34 | ## Standard Configuration 35 | There are two primary pieces of configuration when setting up the `alog` environment: 36 | 37 | 1. **default_level**: This is the level that will be enabled for a given channel when a specific level has not been set in the **filters**. 38 | 39 | 1. **filters**: This is a mapping from channel name to level that allows levels to be set on a per-channel basis. 40 | 41 | ## Formatting 42 | All `alog` implementations support two styles of formatting: 43 | 44 | 1. `pretty`: The **pretty** formatter is designed for development-time logging, making it easy to visually scan your log messages and follow the flow of execution 45 | 46 | 1. `json`: The **json** formatter is designed for production-runtime logging, making it easy to ingest structured representations of your runtime logs into a cloud log monitoring framework such as [Sysdig](https://sysdig.com/) 47 | 48 | As a convenience, a converter is also provided from `json` -> `pretty` so that production logs can be read and visually parsed in a similar manner to development-time logs. The `pretty` -> `json` conversion is not provided since the header formatting for `pretty` logs can be lossy when channel names are truncated. 49 | 50 | ## Implementations 51 | 52 | This section provides the high level details on installing and using each available implementation. 53 | 54 | ### Python 55 | 56 | For full details see [src/python](src/python/README.md) 57 | 58 | #### Installation 59 | 60 | ```sh 61 | pip install alchemy-logging 62 | ``` 63 | 64 | #### Usage 65 | 66 | ```py 67 | import alog 68 | alog.configure(default_level="info", filters="FOO:debug2,BAR:off") 69 | channel = alog.use_channel("FOO") 70 | channel.debug("Hello alog!") 71 | ``` 72 | 73 | ### Typescript 74 | 75 | For full details see [src/ts](src/ts/README.md) 76 | 77 | #### Installation 78 | 79 | ```sh 80 | npm install alchemy-logging 81 | ``` 82 | 83 | #### Usage 84 | 85 | ```ts 86 | import alog from 'alchemy-logging'; 87 | alog.configure('info', 'FOO:debug2,BAR:off'); 88 | alog.debug('FOO', 'Hello alog!'); 89 | ``` 90 | 91 | ### Go 92 | 93 | For full details see [src/go](src/go/README.md) 94 | 95 | #### Installation 96 | 97 | ```sh 98 | go get github.com/IBM/alchemy-logging/src/go/alog 99 | ``` 100 | 101 | #### Usage 102 | 103 | ```go 104 | package main 105 | import ( 106 | "github.com/IBM/alchemy-logging/src/go/alog" 107 | ) 108 | func main() { 109 | alog.Config(alog.INFO, alog.ChannelMap{ 110 | "FOO": alog.DEBUG2, 111 | "BAR": alog.OFF, 112 | }) 113 | alog.Log("FOO", alog.DEBUG, "Hello alog!") 114 | } 115 | ``` 116 | 117 | ### C++ 118 | 119 | For full details see [src/cpp](src/cpp/README.md) 120 | 121 | #### Installation 122 | 123 | In your `CMakeLists.txt` using [CPM](https://github.com/cpm-cmake/CPM.cmake): 124 | 125 | ```cmake 126 | include(cmake/CPM.cmake) 127 | set(ALOG_VERSION main CACHE STRING "The version (point in git history) of alog to use") 128 | CPMAddPackage( 129 | NAME alog 130 | GITHUB_REPOSITORY IBM/alchemy-logging 131 | GIT_TAG ${ALOG_VERSION} 132 | GIT_SHALLOW true 133 | OPTIONS 134 | "BUILD_UNIT_TESTS OFF" 135 | ) 136 | ``` 137 | 138 | #### Usage 139 | 140 | ```cpp 141 | #include 142 | 143 | int main() 144 | { 145 | ALOG_SETUP("info", "FOO:debug2,BAR:off"); 146 | ALOG(FOO, debug2, "Hello alog!"); 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /ci/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the project root 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Get the tag for this release 7 | tag=$(echo $REF | cut -d'/' -f3-) 8 | echo "The tag is ${tag}!!" 9 | 10 | # Parse the tag for prefix and suffix 11 | release_type=$(echo $tag | cut -d'-' -f1) 12 | version=$(echo $tag | cut -d'-' -f2) 13 | 14 | # We explicitly don't want to run with buildkit so that the docker builds happen 15 | # in a linear fashion since our `release_test` stages intentionally don't 16 | # inherit from the stages where the publication happens. 17 | export DOCKER_BUILDKIT=0 18 | 19 | # Dispatch to the various types of releases 20 | if [ "$release_type" == "py" ] 21 | then 22 | cd src/python 23 | docker build . \ 24 | --target=release_test \ 25 | --build-arg PYTHON_RELEASE_VERSION=$version \ 26 | --build-arg PYPI_TOKEN=$PYPI_TOKEN 27 | elif [ "$release_type" == "ts" ] 28 | then 29 | cd src/ts 30 | docker build . \ 31 | --target=release_test \ 32 | --build-arg TS_RELEASE_VERSION=$version \ 33 | --build-arg NPM_TOKEN=$NPM_TOKEN 34 | 35 | elif [ "$release_type" == "cpp" ] 36 | then 37 | cd src/cpp 38 | docker build . \ 39 | --target=release_test \ 40 | --build-arg CPP_RELEASE_VERSION=$tag 41 | 42 | # Go is special and requires valid semantic versioning for its version tags and 43 | # those tags must be scoped by the subdirectory where the go.mod file lives 44 | elif [[ "$tag" =~ src/go/v[0-9]+\.[0-9]+\.[0-9]+.* ]] 45 | then 46 | cd src/go 47 | version=$(echo $tag | rev | cut -d'/' -f 1 | rev) 48 | docker build . \ 49 | --target=release_test \ 50 | --build-arg GO_RELEASE_VERSION=$version 51 | 52 | else 53 | echo "Unknown release type: [$release_type]" 54 | exit 1 55 | fi 56 | -------------------------------------------------------------------------------- /docs/adr/000-template.md: -------------------------------------------------------------------------------- 1 | # ADR: Name of ADR 2 | 3 | 8 | 9 | **Author**: 10 | **Date**: 11 | **Obsoletes**: 12 | 13 |
14 | 15 | TABLE OF CONTENTS 16 | 17 | 18 | - [Objective](#objective) 19 | - [Motivation](#motivation) 20 | - [User Benefit](#user-benefit) 21 | - [Design Proposal](#design-proposal) 22 | - [Alternatives Considered](#alternatives-considered) 23 | - [Performance Implications](#performance-implications) 24 | - [Dependencies](#dependencies) 25 | - [Engineering Impact](#engineering-impact) 26 | - [Platforms and Environments](#platforms-and-environments) 27 | - [Best Practices](#best-practices) 28 | - [Compatibility](#compatibility) 29 | - [User Impact](#user-impact) 30 | - [Detailed Design](#detailed-design) 31 | - [Questions and Discussion Topics](#questions-and-discussion-topics) 32 | 33 |
34 | 35 | ## Objective 36 | 37 | What are we doing and why? What problem will this solve? What are the goals and non-goals? This is your executive summary; keep it short, elaborate below. 38 | 39 | ## Motivation 40 | 41 | Why this is a valuable problem to solve? What background information is needed to show how this design addresses the problem? 42 | 43 | Which users are affected by the problem? Why is it a problem? What data supports this? What related work exists? 44 | 45 | ## User Benefit 46 | 47 | How will users (or other contributors) benefit from this work? 48 | 49 | ## Design Proposal 50 | 51 | This is the meat of the document, where you explain your proposal. If you have multiple alternatives, be sure to use sub-sections for better separation of the idea, and list pros/cons to each approach. If there are alternatives that you have eliminated, you should also list those here, and explain why you believe your chosen approach is superior. 52 | 53 | Make sure you’ve thought through and addressed the following sections. If a section is not relevant to your specific proposal, please explain why, e.g. your ADR addresses a convention or process, not an API. 54 | 55 | ### Alternatives Considered 56 | 57 | - Make sure to discuss the relative merits of alternatives to your proposal. 58 | 59 | ### Performance Implications 60 | 61 | - Do you expect any (speed / memory)? How will you confirm? 62 | - There should be microbenchmarks. Are there? 63 | - There should be end-to-end tests and benchmarks. If there are not (since this is still a design), how will you track that these will be created? 64 | 65 | ### Dependencies 66 | 67 | - Dependencies: does this proposal add any new dependencies? 68 | - Dependent projects: are there other areas or things that this affects? How have you identified these dependencies and are you sure they are complete? If there are dependencies, how are you managing those changes? 69 | 70 | ### Engineering Impact 71 | 72 | - Do you expect changes to binary size / startup time / build time / test times? 73 | - Who will maintain this code? Is this code in its own buildable unit? Can this code be tested in its own? Is visibility suitably restricted to only a small API surface for others to use? 74 | 75 | ### Platforms and Environments 76 | 77 | - Platforms: does this work on all platforms we support? If not, why is that ok? Will it work on embedded/mobile? Does it impact automatic code generation or mobile stripping tooling? Will it work with transformation tools? 78 | - Execution environments (Cloud services, accelerator hardware): what impact do you expect and how will you confirm? 79 | 80 | ### Best Practices 81 | 82 | - Does this proposal change best practices for some aspect of using/developing? How will these changes be communicated/enforced? 83 | 84 | ### Compatibility 85 | 86 | - Does the design conform to the backwards & forwards compatibility 87 | 88 | ### User Impact 89 | 90 | - What are the user-facing changes? How will this feature be rolled out? 91 | 92 | ## Detailed Design 93 | 94 | This section is optional. Elaborate on details if they’re important to understanding the design, but would make it hard to read the proposal section above. 95 | 96 | ## Questions and Discussion Topics 97 | 98 | Seed this with open questions you require feedback on from the ADR process. 99 | -------------------------------------------------------------------------------- /docs/adr/001-releases.md: -------------------------------------------------------------------------------- 1 | # ADR: Releases 2 | 3 | **Author**: Gabe Goodhart 4 | **Date**: 8/13/2021 5 | 6 |
7 | 8 | TABLE OF CONTENTS 9 | 10 | 11 | - [Objective](#objective) 12 | - [Motivation](#motivation) 13 | - [User Benefit](#user-benefit) 14 | - [Design Proposal](#design-proposal) 15 | - [Tagging Releases](#tagging-releases) 16 | - [Semantic Versioning](#semantic-versioning) 17 | - [Version Publication](#version-publication) 18 | - [Alternatives Considered](#alternatives-considered) 19 | - [Dependencies](#dependencies) 20 | - [Engineering Impact](#engineering-impact) 21 | - [Best Practices](#best-practices) 22 | - [User Impact](#user-impact) 23 | - [Questions and Discussion Topics](#questions-and-discussion-topics) 24 | 25 |
26 | 27 | ## Objective 28 | 29 | This ADR establishes the release process for all implementations of the Alchemy Logging framework within this repo. The intent is to have a repeatable process for each language that conforms to the standards of that language while playing nicely in the monorepo. 30 | 31 | ## Motivation 32 | 33 | In order for the various language-specific implementations to be consumed natively, they must each be available via standard package management solutions for the given languages. Publishing these releases must be repeatable and consistent so that changes are rolled out in an orderly fashion and conform to versioning standards within the given language's package community. 34 | 35 | ## User Benefit 36 | 37 | Users will be able to consume `alog` via their standard package managers! They will also be able to rely on the semantic meaning of the releases as they would expect. 38 | 39 | ## Design Proposal 40 | 41 | ### Tagging Releases 42 | 43 | * Each [github release](https://github.com/IBM/alchemy-logging/releases) on this repo will be scoped to a single language implementation. Scoping will be accomplished via the name of the `git tag`. 44 | * This scoping will be managed by a [top level `release.sh` script](https://github.com/IBM/alchemy-logging/blob/main/ci/release.sh) in order to dispatch to the correct language's release process. 45 | * A single [github workflow](https://github.com/IBM/alchemy-logging/blob/main/.github/workflows/release.yml) will handle all releases and run the top-level `release.sh` script 46 | * The scoping rules will be as follows: 47 | * For `python`, `typescript`, and `c++`, and any future implementation that allows it, the tag format will be `[prefix]-[semantic version]`. The prefixes are `py`, `ts`, and `cpp`. 48 | * For `go`, the `go.mod` package manager requires [tags of a specific format](https://blog.golang.org/publishing-go-modules#TOC_3.). Specifically, the tag [must have a prefix](https://golang.org/ref/mod#vcs-version) pointing to the location of the `go.mod` file. We will follow this convention in `alchemy-logging`. 49 | 50 | ### Semantic Versioning 51 | 52 | Each implementation will follow standard [semantic versioning](https://semver.org/) practices. Where appropriate, prerelease metadata can be attached to the `patch` version according to the standards of the given language. 53 | 54 | ### Version Publication 55 | 56 | The following publication targets will be used: 57 | 58 | * `python`: The python package will be [hosted on pypi](https://pypi.org/project/alchemy-logging/) 59 | * **NOTE**: There is a naming collision with the [existing `alog` package](https://pypi.org/project/alog/), so the project name is `alchemy-logging` while the imported module is `alog`. 60 | * `typescript`: The javascript/typescript package will be [hosted on npm](https://www.npmjs.com/package/alchemy-logging) 61 | * `go`: The `go` package will use [the `go.mod` standard](https://blog.golang.org/using-go-modules) and be hosted directly from this github repository. 62 | * `c++`: The `c++` package will use [the `cmake` CPM project](https://github.com/cpm-cmake/CPM.cmake) 63 | * Additionally, users may build and install directly from source using standard `cmake` 64 | 65 | ### Alternatives Considered 66 | 67 | * We could version the entire repo with a single version number 68 | * **PROS**: 69 | * This would make for a much simpler landscape of `git tags` 70 | * It would enforce a tighter coupling between the implementation versions 71 | * **CONS**: 72 | * It would either result in many unnecessary version bumps to versions in languages that have not changed or in skipped version values if only publishing when a given language's implementation has changed 73 | * We could split up the monorepo and implement individual repos for each language implementation 74 | * **PROS**: 75 | * Each implementation would feel more native to the respective language 76 | * Versioning in each independent repo would feel standard 77 | * **CONS**: 78 | * It would introduce difficult maintainence issues in keeping implementations standardized 79 | * It would minimize the primary value proposition of the framework which is that it is a single framework implemented across multiple languages 80 | 81 | ### Dependencies 82 | 83 | * This proposal depends on using `Github Actions` to implement the release workflow 84 | 85 | ### Engineering Impact 86 | 87 | * The responsibility for executing the releases will fall on the codeowners 88 | 89 | ### Best Practices 90 | 91 | * When contributing a change to the project, the Pull Request must indicate which implementation(s) it impacts and what level of semantic version change it requires. 92 | 93 | ### User Impact 94 | 95 | * This ADR will allow users to consume `alog` the way they want 96 | * It will (or has been) rolled out on a per-language basis 97 | 98 | ## Questions and Discussion Topics 99 | 100 | * For the `c++` implementation, we could publish an `.rpm` and/or a `.deb` for various target architecture. Should we? The library is lightweight, but building it is a pain with the boost dependency unless we can solve that and make it lightweight. 101 | * For `typescript`, should we consider also hosting it on [githup package repository](https://github.com/features/packages)? 102 | * For `go`, it _may_ be possible to use [replace directives](https://golang.org/ref/mod#go-mod-file-replace) or some other mechanism to avoid the need for the `/src/go` suffix when importing the module. Is it worth investigation? 103 | * For `python`, we could fully switch the module to be `alchemy_logging`, thus avoiding the collision with [existing `alog` package](https://pypi.org/project/alog/). This would break internal code that imports `alog`, but would likely be a friendlier stance in the community. Is that worth doing? 104 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/cpp/.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | tools/alog_fib_example/build 3 | -------------------------------------------------------------------------------- /src/cpp/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /src/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Set up the basic project skeleton 2 | cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) 3 | set(PROJECT_NAME alog) 4 | project(${PROJECT_NAME} CXX) 5 | 6 | # Specify the C++ standard 7 | set(CMAKE_CXX_STANDARD 11) 8 | set(CMAKE_CXX_STANDARD_REQUIRED True) 9 | 10 | # Option to toggle shared libs 11 | option(BUILD_SHARED_LIBS "Build using shared libraries" ON) 12 | 13 | # Set up CPM to fetch the json dependency and fetch it 14 | include(cmake/CPM.cmake) 15 | CPMAddPackage( 16 | NAME nlohmann_json 17 | GITHUB_REPOSITORY nlohmann/json 18 | VERSION 3.9.1 19 | GIT_SHALLOW true 20 | OPTIONS 21 | "JSON_BuildTests OFF" 22 | ) 23 | 24 | # Add the library 25 | set(library_name alog) 26 | add_library(${library_name} src/logger.cpp) 27 | target_include_directories(${library_name} PUBLIC include) 28 | target_include_directories(${library_name} PUBLIC ${nlohmann_json_SOURCE_DIR}/single_include) 29 | set_target_properties(alog PROPERTIES PUBLIC_HEADER include/alog/logger.hpp) 30 | install( 31 | TARGETS alog 32 | PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/alog 33 | ) 34 | 35 | # Add the unit tests if enabled 36 | option(BUILD_UNIT_TESTS "Build the unit tests" ON) 37 | if (${BUILD_UNIT_TESTS}) 38 | 39 | # Find gtest with local package install 40 | find_package(GTest REQUIRED) 41 | 42 | enable_testing() 43 | set(ut_binary_dir ${CMAKE_BINARY_DIR}/bin/test) 44 | set(ut_exe_name alog_test.ut) 45 | add_executable(${ut_exe_name} tests/logger_test.cpp tests/src/main.cpp) 46 | set_target_properties( 47 | ${ut_exe_name} 48 | PROPERTIES 49 | RUNTIME_OUTPUT_DIRECTORY ${ut_binary_dir} 50 | ) 51 | target_include_directories(${ut_exe_name} PUBLIC tests/include) 52 | target_include_directories(${ut_exe_name} PUBLIC ${GTEST_INCLUDE_DIRS}) 53 | target_link_libraries(${ut_exe_name} ${library_name}) 54 | target_link_libraries(${ut_exe_name} ${GTEST_LIBRARIES}) 55 | target_link_options(${ut_exe_name} PUBLIC "-pthread") 56 | add_test(${ut_exe_name} "${ut_binary_dir}/${ut_exe_name}") 57 | endif() 58 | -------------------------------------------------------------------------------- /src/cpp/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Base ####################################################################### 2 | # 3 | # This phase sets up dependenices for the other phases 4 | ## 5 | FROM gcc:15 as base 6 | 7 | # This image is only for building, so we run as root 8 | WORKDIR /src 9 | 10 | # Install CMake and dependencies 11 | ARG CMAKE_VERSION=3.21.1 12 | RUN true && \ 13 | wget https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.sh && \ 14 | chmod +x cmake-${CMAKE_VERSION}-linux-x86_64.sh && \ 15 | ./cmake-3.21.1-linux-x86_64.sh --skip-license --prefix=/usr/local/ && \ 16 | which cmake && \ 17 | cmake --version && \ 18 | rm cmake-${CMAKE_VERSION}-linux-x86_64.sh && \ 19 | apt-get update -y && \ 20 | apt-get install -y libgtest-dev libgmock-dev && \ 21 | apt-get clean autoclean && \ 22 | apt-get autoremove --yes && \ 23 | true 24 | 25 | ## Test ######################################################################## 26 | # 27 | # This phase runs the unit tests for the library 28 | ## 29 | FROM base as test 30 | COPY . /src 31 | RUN true && \ 32 | mkdir build && \ 33 | cd build && \ 34 | cmake .. -D CMAKE_CXX_FLAGS="-Wall -Werror" && \ 35 | make && \ 36 | ctest && \ 37 | make install && \ 38 | test -e /usr/local/include/alog/logger.hpp && \ 39 | test -e /usr/local/lib/libalog.so && \ 40 | test -e /usr/local/include/nlohmann/json.hpp && \ 41 | true 42 | 43 | ## Release Test ################################################################ 44 | # 45 | # This phase builds the fibonacci example using the tagged release version 46 | ## 47 | FROM base as release_test 48 | ARG CPP_RELEASE_VERSION 49 | COPY ./tools/alog_fib_example /src 50 | RUN true && \ 51 | mkdir build && \ 52 | cd build && \ 53 | cmake .. -DALOG_VERSION=${CPP_RELEASE_VERSION} && \ 54 | make && \ 55 | ./alog_fib_example 5 && \ 56 | true 57 | -------------------------------------------------------------------------------- /src/cpp/ci/develop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the cpp root 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Build the base as the develop shell 7 | build_cmd="docker build" 8 | if docker buildx version 2>&1 > /dev/null 9 | then 10 | build_cmd="docker buildx build --platform=linux/amd64" 11 | fi 12 | $build_cmd . --target=base -t alog-cpp-develop 13 | 14 | # Run with source mounted 15 | docker run --rm -it -v $PWD:/src alog-cpp-develop 16 | -------------------------------------------------------------------------------- /src/cpp/cmake/CPM.cmake: -------------------------------------------------------------------------------- 1 | set(CPM_DOWNLOAD_VERSION 0.32.2) 2 | 3 | if(CPM_SOURCE_CACHE) 4 | # Expand relative path. This is important if the provided path contains a tilde (~) 5 | get_filename_component(CPM_SOURCE_CACHE ${CPM_SOURCE_CACHE} ABSOLUTE) 6 | set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 7 | elseif(DEFINED ENV{CPM_SOURCE_CACHE}) 8 | set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 9 | else() 10 | set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 11 | endif() 12 | 13 | if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) 14 | message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") 15 | file(DOWNLOAD 16 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 17 | ${CPM_DOWNLOAD_LOCATION} 18 | ) 19 | endif() 20 | 21 | include(${CPM_DOWNLOAD_LOCATION}) 22 | -------------------------------------------------------------------------------- /src/cpp/tests/include/unit_testing.h: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | #pragma once 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #include "alog/logger.hpp" 31 | 32 | namespace test 33 | { 34 | 35 | class CAlchemyTestBase : public ::testing::Test 36 | { 37 | protected: 38 | CAlchemyTestBase() {} 39 | virtual ~CAlchemyTestBase() 40 | { 41 | ALOG_RESET(); 42 | } 43 | }; 44 | 45 | #define ALCHEMY_TEST_SUITE(name) class name : public test::CAlchemyTestBase{} 46 | 47 | #define SETTABLE_ARG(type, name, val) \ 48 | type m_ ## name = val; \ 49 | auto name(type v) -> decltype(*this) { m_ ## name = v; return *this; } \ 50 | 51 | #define BEGIN_STEP_COUNT() unsigned ___step = 0; 52 | #define LOG_STEP() \ 53 | ALOG(TEST, debug, "-------- STEP " << ___step++ << " --------"); 54 | 55 | } // end namespace test 56 | -------------------------------------------------------------------------------- /src/cpp/tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | #include 26 | #include 27 | 28 | int main(int argc, char **argv) 29 | { 30 | std::cout << "Hello, testing world" << std::endl; 31 | ::testing::InitGoogleTest(&argc, argv); 32 | return RUN_ALL_TESTS(); 33 | } 34 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Set up the basic project skeleton 2 | cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) 3 | set(PROJECT_NAME alog_fib_example) 4 | project(${PROJECT_NAME} CXX) 5 | 6 | # Specify the C++ standard 7 | set(CMAKE_CXX_STANDARD 11) 8 | set(CMAKE_CXX_STANDARD_REQUIRED True) 9 | 10 | # Use CPM to fetch alog 11 | include(cmake/CPM.cmake) 12 | set(ALOG_VERSION main CACHE STRING "The version (point in git history) of alog to use") 13 | CPMAddPackage( 14 | NAME alog 15 | GITHUB_REPOSITORY IBM/alchemy-logging 16 | GIT_TAG ${ALOG_VERSION} 17 | GIT_SHALLOW true 18 | OPTIONS 19 | "BUILD_UNIT_TESTS OFF" 20 | ) 21 | 22 | # Add the executable 23 | set(exe_name alog_fib_example) 24 | add_executable(${exe_name} main.cpp src/fibonacci.cpp src/util.cpp) 25 | target_link_libraries(${exe_name} alog) 26 | target_include_directories( 27 | ${exe_name} 28 | PRIVATE 29 | include 30 | ${alog_SOURCE_DIR}/include 31 | ) 32 | target_link_options(${exe_name} PUBLIC "-pthread") 33 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/README.md: -------------------------------------------------------------------------------- 1 | # ALOG Example 2 | This sample program is designed to provide an in-depth tutorial of all of the features offered by `ALOG`. The program itself computes fibonacci sequences and has lots of verbose logging throughout. The example can simply be perused, or you can follow the tutorial as outlined in this readme. 3 | 4 | Before exploring this example, you should be familiar with the concepts of [Channels and Levels](https://github.ibm.com/watson-nlu/alog-cpp/tree/mastermaster_nlu#channels-and-levels) outlined in the top-level readme. 5 | 6 | 1. Setup: 7 | * [main.cpp:42](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/main.cpp#L42) 8 | 1. Channels: 9 | 1. Class Channels: [fibonacci.h:42](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/include/fibonacci.h#L42) 10 | 1. Free Channels: [fibonacci.cpp:28](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L28) 11 | 1. Log Statements: 12 | 1. Basic: [main.cpp:72](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/main.cpp#L72) 13 | 1. Channel (i.e. `this`): [fibonacci.cpp:157](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L157) 14 | 1. Map Data: [fibonacci.cpp:68](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L68) 15 | 1. All-In-One: [fibonacci.cpp:95](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L95) 16 | 1. Fatal Errors [main.cpp:101](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/main.cpp#L101) 17 | 1. Log Scopes: 18 | 1. Scoped Block: [main.cpp:84](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/main.cpp#L84) 19 | 1. Function Block: [fibonacci.cpp:132](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L132) 20 | 1. Detail Function Block: [fibonacci.cpp:41](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L41) 21 | 1. Timers: [fibonacci.cpp:49](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L49) [fibonacci.cpp:107](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L107) 22 | 1. Metadata: 23 | 1. [fibonacci.cpp:124](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/src/fibonacci.cpp#L124) 24 | 1. Enabled Blocks: 25 | * [main.cpp:142](https://github.ibm.com/watson-nlu/alog-cpp/blob/master_nlu/tools/alog_fib_example/main.cpp#L142) 26 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/cmake/CPM.cmake: -------------------------------------------------------------------------------- 1 | set(CPM_DOWNLOAD_VERSION 0.32.2) 2 | 3 | if(CPM_SOURCE_CACHE) 4 | # Expand relative path. This is important if the provided path contains a tilde (~) 5 | get_filename_component(CPM_SOURCE_CACHE ${CPM_SOURCE_CACHE} ABSOLUTE) 6 | set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 7 | elseif(DEFINED ENV{CPM_SOURCE_CACHE}) 8 | set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 9 | else() 10 | set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") 11 | endif() 12 | 13 | if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) 14 | message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") 15 | file(DOWNLOAD 16 | https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake 17 | ${CPM_DOWNLOAD_LOCATION} 18 | ) 19 | endif() 20 | 21 | include(${CPM_DOWNLOAD_LOCATION}) 22 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/include/fibonacci.h: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | #pragma once 25 | 26 | // Standard 27 | #include 28 | #include 29 | 30 | // First Party 31 | #include 32 | 33 | namespace fib 34 | { 35 | 36 | /// Type used for a fibonacci sequence 37 | typedef std::vector TFibSequence; 38 | 39 | /// Calculate the Fibonacci sequence to the given length 40 | TFibSequence fib(const unsigned n); 41 | 42 | /// This class is responsible for creating threads that compute the fibonacci 43 | /// sequence and aggregating the results. 44 | class FibonacciCalculator 45 | { 46 | public: 47 | 48 | ////////////////////////////////////////////////////////////////////////////// 49 | // TUTORIAL: // 50 | // // 51 | // The ALOG_USE_CHANNEL macro defines a member function "getChannel()" for // 52 | // the enclosing class, allowing "this" flavor macros to be used in member. // 53 | // functions. It should be preferred over ALOG_USE_CHANNEL_FREE whenever // 54 | // possible. // 55 | ////////////////////////////////////////////////////////////////////////////// 56 | ALOG_USE_CHANNEL(FIB); 57 | 58 | /// Add a sequence length and start the sequence computation 59 | void add_sequence_length(const unsigned n); 60 | 61 | /// Wait for all jobs to complete and return the results 62 | std::vector get_results(); 63 | 64 | private: 65 | 66 | std::vector> m_futures; 67 | }; // end class FibonacciCalculator 68 | 69 | } // end namespace fib 70 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/include/util.h: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | #pragma once 25 | 26 | // Standard 27 | #include 28 | 29 | namespace util 30 | { 31 | 32 | /// Convert a string to lowercase 33 | std::string to_lower(const std::string& input); 34 | 35 | /// Pull a string value from the environment 36 | std::string load_env_string(const std::string& key, const std::string& default_val); 37 | 38 | /// Pull a bool value from the environment 39 | bool load_env_bool(const std::string& key, bool default_val); 40 | 41 | } // end namespace util 42 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/main.cpp: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | // Standard 25 | #include 26 | #include 27 | #include 28 | 29 | // First Party 30 | #include 31 | 32 | // Local 33 | #include "util.h" 34 | #include "fibonacci.h" 35 | 36 | int main(int argc, char *argv[]) 37 | { 38 | // Read configuration from environment 39 | const std::string default_level = util::load_env_string("ALOG_DEFAULT_LEVEL", "info"); 40 | const std::string filters = util::load_env_string("ALOG_FILTERS", "" ); 41 | const bool use_json = util::load_env_bool( "ALOG_USE_JSON", false ); 42 | const bool enable_thread_id = util::load_env_bool( "ALOG_ENABLE_THREAD_ID", false ); 43 | const bool enable_metadata = util::load_env_bool( "ALOG_ENABLE_METADATA", false ); 44 | 45 | ////////////////////////////////////////////////////////////////////////////// 46 | // TUTORIAL: // 47 | // // 48 | // This demonstrates all of the standard configuration features of ALOG. // 49 | // The configuration options are: // 50 | // // 51 | // * Default level: The level to enable for all channels not present in // 52 | // the filters // 53 | // * Filters: Specific channel:level strings to change the enabled level // 54 | // for specific channels // 55 | // * Use JSON: If true, format logs as JSON rather than pretty-print // 56 | // * Thread ID: If true, all logs will contain the thread ID // 57 | // * Metadata: If true, metadata values will be added to each log entry // 58 | ////////////////////////////////////////////////////////////////////////////// 59 | ALOG_SETUP(default_level, filters); 60 | if (use_json) 61 | { 62 | ALOG_USE_JSON_FORMATTER(); 63 | } 64 | if (enable_thread_id) 65 | { 66 | ALOG_ENABLE_THREAD_ID(); 67 | } 68 | if (enable_metadata) 69 | { 70 | ALOG_ENABLE_METADATA(); 71 | } 72 | 73 | ////////////////////////////////////////////////////////////////////////////// 74 | // TUTORIAL: // 75 | // // 76 | // When logging with no configured channel, simply provide the channel as // 77 | // the first argument to the non-this versions of the macros // 78 | ////////////////////////////////////////////////////////////////////////////// 79 | ALOG(MAIN, info, "Logging Configured"); 80 | ALOG(MAIN, debug, "Hello World"); 81 | 82 | // Parse command line args as numbers 83 | std::vector sequence_lengths; 84 | { 85 | //////////////////////////////////////////////////////////////////////////// 86 | // TUTORIAL: // 87 | // // 88 | // When performing a logically grouped set of actions, it can be helpful // 89 | // to have Start/End log blocks to wrap it. For this, use the // 90 | // ALOG_SCOPED_BLOCK macro. // 91 | //////////////////////////////////////////////////////////////////////////// 92 | ALOG_SCOPED_BLOCK(MAIN, debug, "Parsing Command Line"); 93 | 94 | for (int i = 1; i < argc; ++i) 95 | { 96 | ALOG(MAIN, debug2, "Parsing argument " << i); 97 | try 98 | { 99 | int val = std::stoi(argv[i]); 100 | if (val < 0) 101 | { 102 | ////////////////////////////////////////////////////////////////////// 103 | // TUTORIAL: // 104 | // // 105 | // Only log to the `fatal` level when a fatal error has occurred // 106 | // and the application is going down // 107 | ////////////////////////////////////////////////////////////////////// 108 | ALOG(MAIN, fatal, "Invalid negative value [" << val << "]"); 109 | return EXIT_FAILURE; 110 | } 111 | ALOG(MAIN, debug2, "Parsed value [" << val << "]"); 112 | sequence_lengths.push_back(static_cast(val)); 113 | } 114 | catch (const std::invalid_argument&) 115 | { 116 | ALOG(MAIN, fatal, "Invalid argument [" << argv[i] << "]"); 117 | return EXIT_FAILURE; 118 | } 119 | } 120 | if (sequence_lengths.empty()) 121 | { 122 | ALOG(MAIN, fatal, "Must provide at least one sequence length argument"); 123 | return EXIT_FAILURE; 124 | } 125 | } 126 | 127 | // Create the calculator 128 | fib::FibonacciCalculator calculator; 129 | 130 | // For each provided number, compute the sequence using the calculator 131 | { 132 | ALOG_SCOPED_TIMER(MAIN, debug, "Done adding sequences in "); 133 | for (const auto length : sequence_lengths) 134 | { 135 | calculator.add_sequence_length(length); 136 | } 137 | } 138 | 139 | // Aggregate the results and log them 140 | const auto& results = calculator.get_results(); 141 | for (const auto& sequence : results) 142 | { 143 | //////////////////////////////////////////////////////////////////////////// 144 | // TUTORIAL: // 145 | // // 146 | // When constructing a logging string that requires more than a single // 147 | // line, you can wrap the construction in ALOG_IS_ENABLED to avoid the // 148 | // construction work if the channel/level that you will log to is not // 149 | // enabled. // 150 | //////////////////////////////////////////////////////////////////////////// 151 | if (ALOG_IS_ENABLED(MAIN, info)) 152 | { 153 | std::stringstream ss; 154 | ss << "[ "; 155 | for (const auto entry : sequence) 156 | { 157 | ss << entry << " "; 158 | } 159 | ss << "]"; 160 | ALOG(MAIN, info, ss.str()); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/src/fibonacci.cpp: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | // Standard 26 | #include 27 | 28 | // Third Party 29 | #include 30 | 31 | // Local 32 | #include "fibonacci.h" 33 | 34 | using json = nlohmann::json; 35 | 36 | namespace fib 37 | { 38 | 39 | //////////////////////////////////////////////////////////////////////////////// 40 | // TUTORIAL: // 41 | // // 42 | // The ALOG_USE_CHANNEL_FREE macro defines a free-function "getChannel()" // 43 | // which returns the given channel name. // 44 | // // 45 | // WARNING: This MUST be called inside a namespace inside a .cpp file to // 46 | // avoid leaking the definition of getChannel() beyond the intended scope // 47 | //////////////////////////////////////////////////////////////////////////////// 48 | ALOG_USE_CHANNEL_FREE(LFIB) 49 | 50 | TFibSequence fib(const unsigned n) 51 | { 52 | ////////////////////////////////////////////////////////////////////////////// 53 | // TUTORIAL: // 54 | // // 55 | // The ALOG_DETAIL_FUNCTIONthis macro creates a Start/End scope with the // 56 | // name of the current function on the given channel/level. // 57 | ////////////////////////////////////////////////////////////////////////////// 58 | ALOG_DETAIL_FUNCTIONthis(debug, n); 59 | 60 | ////////////////////////////////////////////////////////////////////////////// 61 | // TUTORIAL: // 62 | // // 63 | // For heavy-lifting or long-running functions, we often want to keep track // 64 | // of timing information. In addition, we often want to add information // 65 | // about the results of the function to the timing block. To do so, we // 66 | // create a map pointer that we give to the timer that we can populate at // 67 | // any point before the scope closes. // 68 | ////////////////////////////////////////////////////////////////////////////// 69 | std::shared_ptr timerMap(new json()); 70 | ALOG_SCOPED_TIMERthis(debug, "Computed sequence of length " << n << " in ", timerMap); 71 | 72 | unsigned first = 0; 73 | unsigned second = 1; 74 | unsigned next = 0; 75 | TFibSequence out; 76 | 77 | for ( unsigned c = 0 ; c < n ; ++c ) 78 | { 79 | //////////////////////////////////////////////////////////////////////////// 80 | // TUTORIAL: // 81 | // // 82 | // The ALOG_MAPthis macro logs a key/value map on the given channel/level // 83 | // pair. In this case, we use debug3 since this is a tight-loop log // 84 | // statement. // 85 | //////////////////////////////////////////////////////////////////////////// 86 | ALOG_MAPthis(debug3, (json{ 87 | {"c", c}, 88 | {"first", first}, 89 | {"second", second}, 90 | {"next", next}, 91 | })); 92 | 93 | if ( c <= 1 ) 94 | next = c; 95 | else 96 | { 97 | next = first + second; 98 | first = second; 99 | second = next; 100 | } 101 | // Simulate this being expensive 102 | std::this_thread::sleep_for(std::chrono::milliseconds(next)); 103 | out.push_back(next); 104 | } 105 | 106 | ////////////////////////////////////////////////////////////////////////////// 107 | // TUTORIAL: // 108 | // // 109 | // The ALOGthis macro takes an optional final argument to add a key/value // 110 | // map to the log entry. // 111 | //////////////////////////////////////////////////////////////////////////// 112 | ALOGthis(debug3, "Final variable state", (json{ 113 | {"first", first}, 114 | {"second", second}, 115 | {"next", next}, 116 | })); 117 | 118 | ////////////////////////////////////////////////////////////////////////////// 119 | // TUTORIAL: // 120 | // // 121 | // Here we add a key to the timer map that will be logged at completion. To // 122 | // add the value, we use ALOG_MAP_VALUE to convert to the necessary map // 123 | // value type. // 124 | ////////////////////////////////////////////////////////////////////////////// 125 | (*timerMap)["sequence_length"] = out.size(); 126 | 127 | return out; 128 | } 129 | 130 | // FibonacciCalculator ///////////////////////////////////////////////////////// 131 | 132 | void FibonacciCalculator::add_sequence_length(const unsigned n) 133 | { 134 | ////////////////////////////////////////////////////////////////////////////// 135 | // TUTORIAL: // 136 | // // 137 | // We can add metadata values to any scope that will then be logged with // 138 | // all entries that get created within the scope. // 139 | ////////////////////////////////////////////////////////////////////////////// 140 | ALOG_SCOPED_METADATA("job_number", m_futures.size() + 1); 141 | 142 | ////////////////////////////////////////////////////////////////////////////// 143 | // TUTORIAL: // 144 | // // 145 | // Top-level interface functions use ALOG_FUNCTIONthis to add Start/End // 146 | // function logs on the trace level // 147 | ////////////////////////////////////////////////////////////////////////////// 148 | ALOG_FUNCTIONthis(n); 149 | m_futures.push_back(std::async(&fib, n)); 150 | } 151 | 152 | std::vector FibonacciCalculator::get_results() 153 | { 154 | ////////////////////////////////////////////////////////////////////////////// 155 | // TUTORIAL: // 156 | // // 157 | // When ther are no arguments to a function, you must provide an empty // 158 | // string argument to ALOG_FUNCTIONthis since variadic macros don't support // 159 | // 0 or more arguments (only 1 or more) // 160 | ////////////////////////////////////////////////////////////////////////////// 161 | ALOG_FUNCTIONthis(""); 162 | ALOG_SCOPED_TIMERthis(info, "Finished all jobs in "); 163 | 164 | std::vector out; 165 | unsigned futureNum = 0; 166 | for (auto& future : m_futures) 167 | { 168 | //////////////////////////////////////////////////////////////////////////// 169 | // TUTORIAL: // 170 | // // 171 | // The ALOGthis macro logs a single line on the given channel/level pair. // 172 | // In this case, we use debug2 since this is a detail level log, but not // 173 | // one that will produce overly verbose output. // 174 | //////////////////////////////////////////////////////////////////////////// 175 | ALOGthis(debug2, "Waiting on future " << ++futureNum); 176 | out.push_back(future.get()); 177 | } 178 | return out; 179 | } 180 | 181 | } // end namespace fib 182 | -------------------------------------------------------------------------------- /src/cpp/tools/alog_fib_example/src/util.cpp: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | // Standard 25 | #include 26 | #include 27 | #include 28 | 29 | // Local 30 | #include "util.h" 31 | 32 | namespace util 33 | { 34 | 35 | std::string to_lower(const std::string& input) 36 | { 37 | std::locale loc; 38 | std::stringstream ss; 39 | for (std::string::size_type i=0; i/dev/null 47 | then 48 | all_channels="$all_channels\n$(basename $fname)" 49 | fi 50 | done 51 | 52 | # Make a unique list of channels 53 | unique_channels=$(echo -e ${all_channels} | sort | uniq) 54 | 55 | # Print them all out 56 | for channel in $unique_channels 57 | do 58 | echo $channel 59 | done 60 | -------------------------------------------------------------------------------- /src/go/.gitignore: -------------------------------------------------------------------------------- 1 | *coverage.html 2 | -------------------------------------------------------------------------------- /src/go/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Base ######################################################################## 2 | # 3 | # This phase sets up the working environment for the other phases 4 | ## 5 | FROM golang:1.24-alpine as base 6 | WORKDIR /src 7 | RUN apk add gcc musl-dev curl bash 8 | 9 | ## Test ######################################################################## 10 | # 11 | # This phase runs the unit tests for the library 12 | ## 13 | FROM base as test 14 | COPY . /src 15 | RUN true && \ 16 | go test -coverprofile coverage.html ./... && \ 17 | cd bin/alog_json_converter && \ 18 | go build && \ 19 | cd ../../example/alog_example_server && \ 20 | go build && \ 21 | true 22 | 23 | ## Release Test ################################################################ 24 | # 25 | # This phase builds the example server with redirects removed, forcing it to use 26 | # the tagged version of alog 27 | ## 28 | FROM base as release_test 29 | ARG GO_RELEASE_VERSION 30 | COPY ./example/alog_example_server/ /src/ 31 | COPY ./ci/test_release.sh /src/ 32 | RUN ./test_release.sh 33 | -------------------------------------------------------------------------------- /src/go/alog/helpers_test.go: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | package alog 26 | 27 | import ( 28 | // Standard 29 | "encoding/json" 30 | "fmt" 31 | "os" 32 | "reflect" 33 | "regexp" 34 | "strings" 35 | "sync" 36 | ) 37 | 38 | //////////////////////////////////////////////////////////////////////////////// 39 | // Helpers ///////////////////////////////////////////////////////////////////// 40 | //////////////////////////////////////////////////////////////////////////////// 41 | 42 | // TestWriter - Writer implementation that will keep track of log lines 43 | type TestWriter struct { 44 | mu sync.Mutex 45 | entries *[]string 46 | } 47 | 48 | func (w TestWriter) Write(p []byte) (int, error) { 49 | w.mu.Lock() 50 | n, err := os.Stderr.Write(p) 51 | *w.entries = append(*(w.entries), string(p)) 52 | w.mu.Unlock() 53 | return n, err 54 | } 55 | 56 | // ConfigStdLogWriter - Helper to configure test writer to capture Std log lines 57 | func ConfigStdLogWriter(entries *[]string) { 58 | SetWriter(TestWriter{entries: entries}) 59 | UseStdLogFormatter() 60 | } 61 | 62 | // ConfigJSONLogWriter - Helper to configure test writer to capture json log 63 | // lines 64 | func ConfigJSONLogWriter(entries *[]string) { 65 | SetWriter(TestWriter{entries: entries}) 66 | UseJSONLogFormatter() 67 | } 68 | 69 | // ExpEntry - Helper struct to represent an expected log line 70 | type ExpEntry struct { 71 | channel string 72 | level string 73 | hasGid bool 74 | body string 75 | nIndent int 76 | servicename *string 77 | mapData map[string]interface{} 78 | } 79 | 80 | func matchExp(entry string, exp ExpEntry, verbose bool) bool { 81 | 82 | // Big nasty regex to parse out the parts of a Std formatted log: 83 | // 84 | // Example log line: 85 | // 2017/04/14 19:32:15 [SRVUT:INFO:1] Serving insecure gRPC on port 54321 86 | // 87 | // - "^[0-9/]* [0-9:]*" - Parses the timestamp at the beginning of the line 88 | // - " ([^\\]]*)" - Parses any content after the timestamp, but before the 89 | // bracked header. The only thing that can fall in here is the service name. 90 | // This section is optional, so may be empty 91 | // - "\\[([^:]*):" - Open the bracketed header and parse the channel 92 | // - "([^\\]:]*)" - Parse the level 93 | // - "([^\\]\\s]*)\\]" - Parse the thread id if present (optional) 94 | // - " ([\\s]*)" - Parse the indentation whitespace 95 | // - "([^\\s].*)\n$" - Parse the message to the end of the line 96 | r := regexp.MustCompile("^[0-9/]* [0-9:]* ([^\\]]*)\\[([^:]*):([^\\]:]*)([^\\]\\s]*)\\] ([\\s]*)([^\\s].*)\n$") 97 | 98 | // Parse the log with the regex and make sure there's a (possibly empty) match 99 | // for each of the regex groups. 100 | m := r.FindStringSubmatch(entry) 101 | match := true 102 | if len(m) != 7 { 103 | if verbose { 104 | fmt.Printf("Failed to parse log line [%s]\n", entry) 105 | } 106 | match = false 107 | } else { 108 | if len(m[1]) > 0 && nil == exp.servicename { 109 | if verbose { 110 | fmt.Printf("Got unexpected service name string [%s]\n", m[1]) 111 | } 112 | match = false 113 | } else if len(m[1]) == 0 && nil != exp.servicename { 114 | if verbose { 115 | fmt.Printf("Missing expected service name [%s]\n", *exp.servicename) 116 | } 117 | match = false 118 | } else if len(m[1]) > 0 { 119 | // The service name will be enclosed in angle-brackets if present, so find 120 | // the actual service name by stripping those off 121 | snRexp := regexp.MustCompile("<([^>]*)> ") 122 | snMatch := snRexp.FindStringSubmatch(m[1]) 123 | if len(snMatch) != 2 { 124 | if verbose { 125 | fmt.Println("Missing service name") 126 | } 127 | match = false 128 | } else if snMatch[1] != *exp.servicename { 129 | if verbose { 130 | fmt.Printf("Service name mismatch. Got [%s], Expected [%s]\n", snMatch[1], *exp.servicename) 131 | } 132 | match = false 133 | } 134 | } 135 | if m[2] != exp.channel { 136 | if verbose { 137 | fmt.Printf("Channel string mismatch. Expected [%s], Got [%s]\n", exp.channel, m[1]) 138 | } 139 | match = false 140 | } 141 | if m[3] != exp.level { 142 | if verbose { 143 | fmt.Printf("Level string mismatch. Expected [%s], Got [%s]\n", exp.level, m[2]) 144 | } 145 | match = false 146 | } 147 | if len(m[4]) > 0 && !exp.hasGid { 148 | if verbose { 149 | fmt.Println("Got unexpected GID") 150 | } 151 | match = false 152 | } 153 | if len(m[4]) == 0 && exp.hasGid { 154 | if verbose { 155 | fmt.Println("Missing expected GID") 156 | } 157 | match = false 158 | } 159 | if m[5] != strings.Repeat(GetIndentString(), exp.nIndent) { 160 | if verbose { 161 | fmt.Printf("Indent mismatch. Expected [%s], Got [%s]\n", m[4], strings.Repeat(GetIndentString(), exp.nIndent)) 162 | match = false 163 | } 164 | } 165 | if m[6] != exp.body { 166 | if verbose { 167 | fmt.Printf("Body string mismatch. Expected [%s], Got [%s]\n", exp.body, m[4]) 168 | } 169 | match = false 170 | } 171 | } 172 | return match 173 | } 174 | 175 | // VerifyLogs - Verify that the expected STD log lines were logged 176 | func VerifyLogs(entries []string, expected []ExpEntry) bool { 177 | if len(entries) != len(expected) { 178 | fmt.Printf("Length mismatch: Expected %d, Got %d\n", len(expected), len(entries)) 179 | return false 180 | } 181 | match := true 182 | for i, entry := range entries { 183 | if !matchExp(entry, expected[i], true) { 184 | fmt.Printf("Match failed for entry [%d]\n", i) 185 | match = false 186 | } 187 | } 188 | return match 189 | } 190 | 191 | // VerifyLogsUnordered - Verify that the expecte log lines were logged, 192 | // regardless of order 193 | func VerifyLogsUnordered(entries []string, expected []ExpEntry) bool { 194 | if len(entries) != len(expected) { 195 | fmt.Printf("Length mismatch: Expected %d, Got %d\n", len(expected), len(entries)) 196 | return false 197 | } 198 | match := true 199 | for _, exp := range expected { 200 | foundMatch := false 201 | for _, entry := range entries { 202 | if matchExp(entry, exp, false) { 203 | foundMatch = true 204 | break 205 | } 206 | } 207 | if !foundMatch { 208 | fmt.Printf("No match found for expected entry [%v]\n", exp) 209 | match = false 210 | } 211 | } 212 | return match 213 | } 214 | 215 | // VerifyJSONLogs - Verify that the expected JSON log lines were logged 216 | func VerifyJSONLogs(entries []string, expected []ExpEntry) bool { 217 | if len(entries) != len(expected) { 218 | fmt.Printf("Length mismatch: Expected %d, Got %d\n", len(expected), len(entries)) 219 | return false 220 | } 221 | match := true 222 | for i, entry := range entries { 223 | if !matchExpJSON(entry, expected[i]) { 224 | fmt.Printf("Match failed for entry [%d]\n", i) 225 | match = false 226 | } 227 | } 228 | return match 229 | } 230 | 231 | func matchExpJSON(entry string, expected ExpEntry) bool { 232 | 233 | // Parse to a LogEntry 234 | var logEntry LogEntry 235 | if le, err := JSONToLogEntry(entry); nil != err { 236 | fmt.Printf("Failed to unmarshal entry [%s]: %v\n", entry, err.Error()) 237 | return false 238 | } else if nil == le { 239 | fmt.Printf("Failed to parse LogEntry [%s]\n", entry) 240 | return false 241 | } else { 242 | logEntry = *le 243 | } 244 | 245 | // Check fields 246 | match := true 247 | if string(logEntry.Channel) != expected.channel { 248 | fmt.Printf("Channel string mismatch. Got [%s], expected [%s]\n", string(logEntry.Channel), expected.channel) 249 | match = false 250 | } 251 | if LevelToHumanString(logEntry.Level) != expected.level { 252 | fmt.Printf("Level string mismatch. Got [%s], expected [%s]\n", LevelToHumanString(logEntry.Level), expected.level) 253 | match = false 254 | } 255 | if logEntry.Format != expected.body { 256 | fmt.Printf("Message mismatch. Got [%s], expected [%s]\n", logEntry.Format, expected.body) 257 | match = false 258 | } 259 | if logEntry.NIndent != expected.nIndent { 260 | fmt.Printf("Indent mismatch. Got [%d], expected [%d]\n", logEntry.NIndent, expected.nIndent) 261 | match = false 262 | } 263 | 264 | // Optional GID 265 | if expected.hasGid && nil == logEntry.GoroutineID { 266 | fmt.Printf("Missing expected GID\n") 267 | match = false 268 | } else if !expected.hasGid && nil != logEntry.GoroutineID { 269 | fmt.Printf("Got GID when none expected\n") 270 | match = false 271 | } 272 | 273 | // Optional service name 274 | if nil == expected.servicename && len(logEntry.Servicename) != 0 { 275 | fmt.Printf("Got unexpected service name [%s]", logEntry.Servicename) 276 | match = false 277 | } else if nil != expected.servicename && *expected.servicename != logEntry.Servicename { 278 | fmt.Printf("Service name mismatch. Got [%s], expected [%s]\n", logEntry.Servicename, *expected.servicename) 279 | match = false 280 | } 281 | 282 | // Map data 283 | if len(expected.mapData) != len(logEntry.MapData) { 284 | fmt.Printf("Mismatched mapData length. Got [%d], expected [%d]\n", len(logEntry.MapData), len(expected.mapData)) 285 | match = false 286 | } 287 | for k, v := range expected.mapData { 288 | if gotVal, ok := logEntry.MapData[k]; !ok { 289 | fmt.Printf("Missing expected mapData entry [%s]\n", k) 290 | match = false 291 | } else if gotNumVal, ok := gotVal.(json.Number); ok { 292 | if gotNumVal.String() != fmt.Sprintf("%v", v) { 293 | fmt.Printf("Value mismatch for numerical mapData entry [%s]. Got: %v, expected %v\n", k, gotVal, v) 294 | match = false 295 | } 296 | } else if !reflect.DeepEqual(v, gotVal) { 297 | fmt.Printf("Value mismatch for mapData entry [%s]. Got: [%v - %s], expected [%v - %s]\n", k, gotVal, reflect.TypeOf(gotVal), v, reflect.TypeOf(v)) 298 | match = false 299 | } 300 | } 301 | 302 | return match 303 | } 304 | 305 | // ValidateChannelMap - Compare an expected channel map to a configured map 306 | func ValidateChannelMap(got, expected ChannelMap) bool { 307 | ch := UseChannel("TEST") 308 | res := true 309 | if len(got) != len(expected) { 310 | ch.Log(ERROR, "Channel map length mismatch. Got %d, Expected %d", len(got), len(expected)) 311 | res = false 312 | } 313 | for k, expVal := range expected { 314 | if gotVal, ok := got[k]; !ok { 315 | ch.Log(ERROR, "Missing expected key [%s]", k) 316 | res = false 317 | } else if gotVal != expVal { 318 | ch.Log(ERROR, "Incorrect level for [%s]. Got [%s], Expected [%s]", k, 319 | LevelToHumanString(gotVal), LevelToHumanString(expVal)) 320 | res = false 321 | } 322 | } 323 | return res 324 | } 325 | -------------------------------------------------------------------------------- /src/go/bin/alog_json_converter/.gitignore: -------------------------------------------------------------------------------- 1 | alog_json_converter 2 | -------------------------------------------------------------------------------- /src/go/bin/alog_json_converter/main.go: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | package main 26 | 27 | import ( 28 | "bufio" 29 | "flag" 30 | "fmt" 31 | "github.com/IBM/alchemy-logging/src/go/alog" 32 | "os" 33 | ) 34 | 35 | func main() { 36 | 37 | // Flag to indicate input source (default to stdin) 38 | inputFile := flag.String( 39 | "input-file", 40 | "", 41 | "Input file with JSON log data. If none set, read from stdin.", 42 | ) 43 | 44 | // Flag to indcate output file (default to stdout) 45 | outputFile := flag.String( 46 | "output-file", 47 | "", 48 | "Output file to write log lines to. If none set, write to stdout.", 49 | ) 50 | 51 | flag.Parse() 52 | 53 | // Set up input reader 54 | reader := os.Stdin 55 | if nil != inputFile && len(*inputFile) > 0 { 56 | if fReader, err := os.Open(*inputFile); nil != err { 57 | fmt.Printf("Error opening input file: %v\n", err) 58 | os.Exit(1) 59 | } else { 60 | reader = fReader 61 | } 62 | } 63 | bufReader := bufio.NewReader(reader) 64 | 65 | // Set up the output writer 66 | writer := os.Stdout 67 | if nil != outputFile && len(*outputFile) > 0 { 68 | if fout, err := os.Create(*outputFile); nil != err { 69 | fmt.Printf("Error opening output file: %v\n", err) 70 | os.Exit(1) 71 | } else { 72 | writer = fout 73 | } 74 | } 75 | bufWriter := bufio.NewWriter(writer) 76 | 77 | // Read each line from input and write to output 78 | for { 79 | if line, err := bufReader.ReadString('\n'); nil != err { 80 | os.Exit(0) 81 | } else { 82 | if outlines, err := alog.JSONToPlainText(line); nil != err { 83 | fmt.Printf("Error converting line [%s]\n", line) 84 | fmt.Printf("%v\n", err) 85 | } else { 86 | for _, outline := range outlines { 87 | bufWriter.WriteString(outline) 88 | bufWriter.Flush() 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/go/bin/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IBM/alchemy-logging/src/go/bin 2 | 3 | go 1.16 4 | 5 | replace github.com/IBM/alchemy-logging/src/go => ../ 6 | 7 | require github.com/IBM/alchemy-logging/src/go v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /src/go/bin/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /src/go/ci/develop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the cpp root 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Build the base as the develop shell 7 | docker build . --target=base -t alog-go-develop 8 | 9 | # Run with source mounted 10 | docker run --rm -it -v $PWD:/src alog-go-develop 11 | -------------------------------------------------------------------------------- /src/go/ci/test_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ################################################################################ 4 | # This script tests a given release to make sure it has propagated through the 5 | # necessary proxies and can successfully build and run the test server. It is 6 | # designed to be run inside the docker build only! 7 | ################################################################################ 8 | 9 | # Set up a basic go.mod file to build the server 10 | echo "module alog_example_server" > go.mod 11 | 12 | # Try to fetch the release until it either times out or we succeed 13 | retry_sleep=10 14 | total_time=0 15 | timeout=600 16 | until [ $total_time -eq $timeout ] || \ 17 | go get github.com/IBM/alchemy-logging/src/go/alog@${GO_RELEASE_VERSION} 18 | do 19 | sleep $retry_sleep 20 | total_time=$(expr $total_time + $retry_sleep) 21 | done 22 | 23 | # If we did not successfully find the release, don't let it find any-old one 24 | if ! grep ${GO_RELEASE_VERSION} go.mod 25 | then 26 | echo "Failed to find release ${GO_RELEASE_VERSION}" 27 | exit 1 28 | fi 29 | 30 | # Tidy up and build then test the server 31 | go mod tidy 32 | go build 33 | ./test_example_server.sh 34 | -------------------------------------------------------------------------------- /src/go/example/alog_example_server/.gitignore: -------------------------------------------------------------------------------- 1 | alog_example_server 2 | -------------------------------------------------------------------------------- /src/go/example/alog_example_server/main.go: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | package main 26 | 27 | import ( 28 | "flag" 29 | "github.com/IBM/alchemy-logging/src/go/alog" 30 | "net/http" 31 | "strings" 32 | ) 33 | 34 | var ch = alog.UseChannel("MAIN") 35 | 36 | func main() { 37 | 38 | // Get a port from the command line 39 | listenPort := flag.String( 40 | "port", 41 | "54321", 42 | "port bound to this service") 43 | 44 | // Set up logging from command line 45 | logFlags := alog.GetFlags() 46 | flag.Parse() 47 | if err := alog.ConfigureFromFlags(logFlags); nil != err { 48 | alog.Fatalf("MAIN", alog.FATAL, err.Error()) 49 | } 50 | ch.Log(alog.INFO, "Hello World!") 51 | 52 | // Bind dynamic log handler 53 | http.HandleFunc("/logging", alog.DynamicHandler) 54 | 55 | // Bind simple function that does some logging 56 | http.HandleFunc("/demo", func(w http.ResponseWriter, r *http.Request) { 57 | ch := alog.UseChannel("HNDLR") 58 | defer ch.LogScope(alog.TRACE, "Handling /demo").Close() 59 | 60 | ch.Log(alog.WARNING, "WATCH OUT!") 61 | ch.Log(alog.INFO, "Standard stuff...") 62 | if ch.IsEnabled(alog.DEBUG) { 63 | ch.Log(alog.DEBUG, "Query Params:") 64 | r.ParseForm() 65 | for k, vals := range r.Form { 66 | ch.Log(alog.DEBUG, " * %s: %s", k, strings.Join(vals, ", ")) 67 | } 68 | } 69 | w.WriteHeader(http.StatusOK) 70 | }) 71 | 72 | // Start serving requests 73 | ch.Log(alog.FATAL, "%s", http.ListenAndServe(":"+*listenPort, nil)) 74 | } 75 | -------------------------------------------------------------------------------- /src/go/example/alog_example_server/test_example_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ################################################################################ 4 | # 5 | # This is a simple script that takes the example server out for a walk and makes 6 | # a few curl calls against it 7 | ################################################################################ 8 | 9 | # Failed calls fail the script 10 | set -euo pipefail 11 | 12 | # Run from the package directory 13 | cd $(dirname ${BASH_SOURCE[0]}) 14 | 15 | # Run the server in the background 16 | port=55544 17 | ./alog_example_server \ 18 | -log.default-level info \ 19 | -log.goroutine-id \ 20 | -log.service-name example-server \ 21 | -port $port & 22 | server_pid=$! 23 | sleep 1 24 | 25 | # Make some calls to the server 26 | timeout=2 27 | curl -s "http://localhost:$port/demo" 28 | curl -s "http://localhost:$port/logging?default_level=debug3&timeout=$timeout" 29 | curl -s "http://localhost:$port/demo" 30 | 31 | # Wait for the timeout to expire 32 | sleep $timeout 33 | 34 | # Kill the server 35 | pkill -15 $server_pid || true 36 | -------------------------------------------------------------------------------- /src/go/example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IBM/alchemy-logging/src/go/example 2 | 3 | go 1.16 4 | 5 | replace github.com/IBM/alchemy-logging/src/go => ../ 6 | 7 | require github.com/IBM/alchemy-logging/src/go v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /src/go/example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /src/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IBM/alchemy-logging/src/go 2 | 3 | go 1.16 4 | 5 | require github.com/stretchr/testify v1.7.0 // indirect 6 | -------------------------------------------------------------------------------- /src/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /src/python/.gitignore: -------------------------------------------------------------------------------- 1 | alchemy_logging.egg-info 2 | build 3 | dist 4 | __pycache__/ 5 | htmlcov 6 | .coverage 7 | # output files from util tests 8 | tests/**/*_copy.py 9 | **/*.pyc 10 | -------------------------------------------------------------------------------- /src/python/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Alchemy Logging (alog) - Python - Contributor Guide 2 | 3 | For general contribution practices, please refer to [the overall project CONTRIBUTING.md](../../CONTRIBUTING.md). 4 | 5 | ## Unit Testing 6 | 7 | You can run the unit tests as follows: 8 | 9 | ```sh 10 | # One-time setup to get the development environment set up 11 | PYTHON_RELEASE_VERSION=0.0.0 python setup.py develop 12 | 13 | # Run the tests 14 | ./ci/run-tests.sh 15 | ``` 16 | -------------------------------------------------------------------------------- /src/python/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Base ######################################################################## 2 | # 3 | # This phase sets up dependencies for the other phases 4 | ## 5 | ARG PYTHON_VERSION=3.6 6 | FROM python:${PYTHON_VERSION}-slim as base 7 | 8 | # This image is only for building, so we run as root 9 | WORKDIR /src 10 | 11 | # Install build, test, andn publish dependencies 12 | COPY requirements_test.txt /src/requirements_test.txt 13 | RUN true && \ 14 | pip install pip --upgrade && \ 15 | pip install twine && \ 16 | pip install -r /src/requirements_test.txt && \ 17 | true 18 | 19 | ## Test ######################################################################## 20 | # 21 | # This phase runs the unit tests for the library 22 | ## 23 | FROM base as test 24 | COPY . /src 25 | RUN true && \ 26 | ./ci/run-tests.sh && \ 27 | RELEASE_DRY_RUN=true PYTHON_RELEASE_VERSION=0.0.0 \ 28 | ./ci/publish.sh && \ 29 | true 30 | 31 | ## Release ##################################################################### 32 | # 33 | # This phase builds the release and publishes it to pypi 34 | ## 35 | FROM test as release 36 | ARG PYPI_TOKEN 37 | ARG PYTHON_RELEASE_VERSION 38 | ARG RELEASE_DRY_RUN 39 | RUN ./ci/publish.sh 40 | 41 | ## Release Test ################################################################ 42 | # 43 | # This phase installs the indicated version from PyPi and runs the unit tests 44 | # against the installed version. 45 | ## 46 | FROM base as release_test 47 | ARG PYTHON_RELEASE_VERSION 48 | ARG RELEASE_DRY_RUN 49 | COPY ./tests /src/tests 50 | COPY ./ci/run-tests.sh /src/ci/run-tests.sh 51 | RUN true && \ 52 | ([ "$RELEASE_DRY_RUN" != "true" ] && sleep 30 || true) && \ 53 | pip cache purge && \ 54 | pip install alchemy-logging==${PYTHON_RELEASE_VERSION} && \ 55 | ./ci/run-tests.sh ./tests/test_alog.py && \ 56 | true 57 | -------------------------------------------------------------------------------- /src/python/README.md: -------------------------------------------------------------------------------- 1 | # Alchemy Logging (alog) - Python 2 | The `alog` framework provides tunable logging with easy-to-use defaults and power-user capabilities. The mantra of `alog` is **"Log Early And Often"**. To accomplish this goal, `alog` makes it easy to enable verbose logging at develop/debug time and trim the verbosity at production run time. 3 | 4 | ## Setup 5 | To use the `alog` module, simply install it with `pip`: 6 | 7 | ```sh 8 | pip install alchemy-logging 9 | ``` 10 | 11 | ## Channels and Levels 12 | The primary components of the framework are **channels** and **levels** which allow for each log statement to be enabled or disabled when appropriate. 13 | 14 | 1. **Levels**: Each logging statement is made at a specific level. Levels provide sequential granularity, allowing detailed debugging statements to be placed in the code without clogging up the logs at runtime. The sequence of levels and their general usage is as follows: 15 | 16 | 1. `off`: Disable the given channel completely 17 | 1. `fatal`: A fatal error has occurred. Any behavior after this statement should be regarded as undefined. 18 | 1. `error`: An unrecoverable error has occurred. Any behavior after this statement should be regarded as undefined unless the error is explicitly handled. 19 | 1. `warning`: A recoverable error condition has come up that the service maintainer should be aware of. 20 | 1. `info`: High-level information that is valuable at runtime under moderate load. 21 | 1. `trace`: Used to log begin/end of functions for debugging code paths. 22 | 1. `debug`: High-level debugging statements such as function parameters. 23 | 1. `debug1`: High-level debugging statements. 24 | 1. `debug2`: Mid-level debugging statements such as computed values. 25 | 1. `debug3`: Low-level debugging statements such as computed values inside loops. 26 | 1. `debug4`: Ultra-low-level debugging statements such as data dumps and/or statements inside multiple nested loops. 27 | 28 | 1. **Channels**: Each logging statement is made to a specific channel. Channels are independent of one another and allow for logical grouping of log messages by functionality. A channel can be any string. A channel may have a specific **level** assigned to it, or it may use the configured default level if it is not given a specific level filter. 29 | 30 | Using this combination of **Channels** and **Levels**, you can fine-tune what log statements are enabled when you run your application under different circumstances. 31 | 32 | ## Usage 33 | 34 | ### Configuration 35 | 36 | ```py 37 | import alog 38 | 39 | if __name__ == "__main__": 40 | alog.configure(default_level="info", filters="FOO:debug,BAR:off") 41 | ``` 42 | 43 | In this example, the channel `"FOO"` is set to the `debug` level, the channel `"BAR"` is fully disabled, and all other channels are set to use the `INFO` level. 44 | 45 | In addition to the above, the `configure` function also supports the following arguments: 46 | 47 | * `formatter`: May be `"pretty"`, `"json"`, or any class derived from `AlogFormatterBase` 48 | * `thread_id`: Bool indicating whether or not to include a unique thread ID with the logging header (`pretty`) or structure (`json`). 49 | * `handler_generator`: This allows users to provide their own output handlers and replace the standard handler that sends log messages to `stderr`. See [the `logging` documentation](https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers) for details. 50 | 51 | ### Logging Functions 52 | For each log level, there are two functions you can use to create log lines: The standard `logging` package function, or the corresponding `alog.use_channel(...).` function. The former will always log to the `root` channel while the later requires that 53 | a channel string be specified via `use_channel()`. 54 | 55 | ```py 56 | import alog 57 | import logging 58 | 59 | def foo(age): 60 | alog.use_channel("FOO").debug3( 61 | "Debug3 line on the FOO channel with an int value %d!", age 62 | ) 63 | logging.debug("debug line on the MAIN channel") 64 | ``` 65 | 66 | ### Channel Log 67 | In a given portion of code, it often makes sense to have a common channel that is used by many logging statements. Re-typing the channel name can be cumbersome and error-prone, so the concept of the **Channel Log** helps to eliminate this issue. To create a Channel Log, call the `use_channel` function. This gives you a handle to a channel log which has all of the same standard log functions as the top-level `alog`, but without the requirement to specify a channel. For example: 68 | 69 | ```py 70 | import alog 71 | 72 | log = alog.use_channel("FOO") 73 | 74 | def foo(age): 75 | log.info("Hello Logging World! I am %d years old", age) 76 | ``` 77 | 78 | **NOTE**: In this (python) implementation, this is simply a wrapper around `logging.getLogger()` 79 | 80 | ### Extra Log Information 81 | 82 | There are several other types of information that `alog` supports adding to log records: 83 | 84 | #### Log Codes 85 | 86 | This is an optional argument to all logging functions which adds a specified code to the record. It can be useful for particularly high-profile messages (such as per-request error summaries in a server) that you want to be able to track in a programmatic way. The only requirement for a `log_code` is that it begin with `<` and end with `>`. The log code always comes before the `message`. For example: 87 | 88 | ```py 89 | ch = alog.use_channel("FOO") 90 | ch.debug("", "Logging is fun!") 91 | ``` 92 | 93 | #### Dict Data 94 | 95 | Sometimes, it's useful to log structured key/value pairs in a record, rather than a plain-text message, even when using the `pretty` output formatter. To do this, simply use a `dict` in place of a `str` in the `message` argument to the logging function. For example: 96 | 97 | ```py 98 | ch = alog.use_channel("FOO") 99 | ch.debug({"foo": "bar"}) 100 | ``` 101 | 102 | When a `dict` is logged with the `json` formatter enabled, all key/value pairs are added as key/value pairs under the top-level `message` key. 103 | 104 | ### Log Contexts 105 | One of the most common uses for logging is to note events when a certain block of code executes. To facilitate this, `alog` has the concept of log contexts. The two primary contexts that `alog` supports are: 106 | 107 | * `ContextLog`: This [contextmanager](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager) logs a `START:` message when the context starts and an `END:` message when the context ends. All messages produced within the same thread inside of the context will have an incremented level of indentation. 108 | 109 | ```py 110 | import alog 111 | 112 | alog.configure("debug2") 113 | log = alog.use_channel("DEMO") 114 | 115 | with alog.ContextLog(log.info, "Doing some work"): 116 | log.debug("Deep in the muck!") 117 | ``` 118 | 119 | ``` 120 | 2021-07-29T19:09:03.819422 [DEMO :INFO] BEGIN: Doing some work 121 | 2021-07-29T19:09:03.820079 [DEMO :DBUG] Deep in the muck! 122 | 2021-07-29T19:09:03.820178 [DEMO :INFO] END: Doing some work 123 | ``` 124 | 125 | * `ContextTimer`: This [contextmanager](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager) starts a timer when the context begins and logs a message with the duration when the context ends. 126 | 127 | ```py 128 | import alog 129 | import time 130 | 131 | alog.configure("debug2") 132 | log = alog.use_channel("DEMO") 133 | 134 | with alog.ContextTimer(log.info, "Slow work finished in: "): 135 | log.debug("Starting the slow work") 136 | time.sleep(1) 137 | ``` 138 | 139 | ``` 140 | 2021-07-29T19:12:00.887949 [DEMO :DBUG] Starting the slow work 141 | 2021-07-29T19:12:01.890839 [DEMO :INFO] Slow work finished in: 0:00:01.002793 142 | ``` 143 | 144 | ### Function Decorators 145 | In addition to arbitrary blocks of code that you may wish to scope or time, a very common use case for logging contexts is to provide function tracing. To this end, `alog` provides two useful function decorators: 146 | 147 | * `@logged_function`: This [decorator](https://www.python.org/dev/peps/pep-0318/) wraps the `ContextLog` and provides a `START`/`END` scope where the message is prepopulated with the name of the function. 148 | 149 | ```py 150 | import alog 151 | 152 | alog.configure("debug") 153 | log = alog.use_channel("DEMO") 154 | 155 | @alog.logged_function(log.trace) 156 | def foo(): 157 | log.debug("This is a test") 158 | 159 | foo() 160 | ``` 161 | 162 | ``` 163 | 2021-07-29T19:16:40.036119 [DEMO :TRCE] BEGIN: foo() 164 | 2021-07-29T19:16:40.036807 [DEMO :DBUG] This is a test 165 | 2021-07-29T19:16:40.036915 [DEMO :TRCE] END: foo() 166 | ``` 167 | 168 | * `@timed_function`: This [decorator](https://www.python.org/dev/peps/pep-0318/) wraps the `ContextTimer` and performs a scoped timer on the entire function. 169 | 170 | ```py 171 | import alog 172 | import time 173 | 174 | alog.configure("debug") 175 | log = alog.use_channel("DEMO") 176 | 177 | @alog.timed_function(log.trace) 178 | def foo(): 179 | log.debug("This is a test") 180 | time.sleep(1) 181 | 182 | foo() 183 | ``` 184 | 185 | ``` 186 | 2021-07-29T19:19:47.468428 [DEMO :DBUG] This is a test 187 | 2021-07-29T19:19:48.471788 [DEMO :TRCE] 0:00:01.003284 188 | ``` 189 | 190 | ## Tip 191 | - Visual Studio Code (VSCode) users can take advantage of [alchemy-logging extension](https://marketplace.visualstudio.com/items?itemName=Gaurav-Kumbhat.alog-code-generator) that provides automatic log code generation and insertion. 192 | -------------------------------------------------------------------------------- /src/python/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | """Forward imports from module for usage as a submodule. 25 | """ 26 | 27 | from .alog import * 28 | -------------------------------------------------------------------------------- /src/python/alog/__init__.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | """Alchemy Logging in python""" 25 | 26 | # Core components 27 | from .alog import ( 28 | AlogFormatterBase, 29 | AlogJsonFormatter, 30 | AlogPrettyFormatter, 31 | ContextLog, 32 | ContextTimer, 33 | FnLog, 34 | FunctionLog, 35 | ScopedLog, 36 | ScopedTimer, 37 | configure, 38 | logged_function, 39 | timed_function, 40 | use_channel, 41 | ) 42 | 43 | # Exposed details 44 | from .alog import g_alog_level_to_name as _level_to_name 45 | from .alog import g_alog_level_to_name as _name_to_level 46 | from .protocols import ALogLoggerProtocol 47 | -------------------------------------------------------------------------------- /src/python/ci/develop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the cpp root 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Build the base as the develop shell 7 | docker build . --target=base -t alog-py-develop $@ 8 | 9 | # Run with source mounted 10 | docker run --rm -it -v $PWD:/src --entrypoint /bin/bash -w /src alog-py-develop 11 | -------------------------------------------------------------------------------- /src/python/ci/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the base of the python directory 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Clear out old publication files in case they're still around 7 | rm -rf build dist alchemy_logging.egg-info/ 8 | 9 | # Build 10 | python setup.py sdist bdist_wheel 11 | 12 | # Publish to PyPi 13 | if [ "${RELEASE_DRY_RUN}" != "true" ] 14 | then 15 | twine upload \ 16 | --username "__token__" \ 17 | --password "$PYPI_TOKEN" \ 18 | dist/* 19 | else 20 | echo "Release DRY RUN" 21 | fi 22 | 23 | # Clean up 24 | rm -rf build dist alchemy_logging.egg-info/ 25 | -------------------------------------------------------------------------------- /src/python/ci/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 5 | cd "$BASE_DIR" 6 | 7 | python3 -m pytest \ 8 | --cov=alog \ 9 | --cov=util \ 10 | --cov-report=term \ 11 | --cov-report=html \ 12 | -Werror "$@" 13 | -------------------------------------------------------------------------------- /src/python/requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest>=7.0.1,<9 2 | pytest-cov>=4.0.0,<7 3 | -------------------------------------------------------------------------------- /src/python/setup.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | """A setuptools setup module for py_scripting 25 | """ 26 | 27 | from setuptools import setup 28 | import os 29 | 30 | # Read the README to provide the long description 31 | python_base = os.path.abspath(os.path.dirname(__file__)) 32 | with open(os.path.join(python_base, "README.md"), "r") as handle: 33 | long_description = handle.read() 34 | 35 | # Read version from the env 36 | version = os.environ.get("PYTHON_RELEASE_VERSION") 37 | assert version is not None, "Must set PYTHON_RELEASE_VERSION" 38 | 39 | setup( 40 | name="alchemy-logging", 41 | version=version, 42 | description="A wrapper around the logging package to provide Alchemy Logging functionality", 43 | long_description=long_description, 44 | long_description_content_type="text/markdown", 45 | url="https://github.com/IBM/alchemy-logging", 46 | author="Gabe Goodhart", 47 | author_email="gabe.l.hart@gmail.com", 48 | license="MIT", 49 | classifiers=[ 50 | "Intended Audience :: Developers", 51 | "Topic :: Software Development :: User Interfaces", 52 | "Programming Language :: Python :: 3", 53 | "Programming Language :: Python :: 3.5", 54 | "Programming Language :: Python :: 3.6", 55 | "Programming Language :: Python :: 3.7", 56 | "Programming Language :: Python :: 3.8", 57 | "Programming Language :: Python :: 3.9", 58 | "Programming Language :: Python :: 3.10", 59 | ], 60 | keywords="logging", 61 | packages=["alog"], 62 | install_requires=["typing_extensions;python_version<'3.8'"], 63 | ) 64 | -------------------------------------------------------------------------------- /src/python/tests/fixtures/duplicate_replacement/test_case_1.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | '' 25 | ' ' 26 | '' 27 | ' ' 28 | '' 29 | -------------------------------------------------------------------------------- /src/python/tests/fixtures/duplicate_replacement/test_case_2.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | '' 25 | '' 26 | '' 27 | '' 28 | -------------------------------------------------------------------------------- /src/python/tests/fixtures/placeholders/test_case_3.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | '' 25 | -------------------------------------------------------------------------------- /src/python/tests/fixtures/placeholders/test_case_4.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | '' 25 | -------------------------------------------------------------------------------- /src/python/tests/test_alog_clean_import.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | 25 | # Standard 26 | import logging 27 | import io 28 | import re 29 | 30 | # Local 31 | import alog 32 | 33 | def test_log_before_configure(): 34 | """Make sure that no exception is thrown if a high-order log function is 35 | called before calling configure() 36 | """ 37 | ch = alog.use_channel("TEST") 38 | ch.debug2("No throw") 39 | 40 | 41 | def test_default_log_configuration_works(): 42 | '''We need to run this in a process that has a clean set of imports so that 43 | we can evaluate the import behavior. Using importlib.reload in the main 44 | process does not actually rerun the import-time code in alog. 45 | ''' 46 | # Set up some standard logging with custom formatting 47 | log_stream = io.StringIO() 48 | stream_handler = logging.StreamHandler(stream=log_stream) 49 | fmt = '%(asctime)s [%(levelname)s] %(message)s' 50 | logging.basicConfig( 51 | level=logging.INFO, 52 | format=fmt, 53 | handlers=[stream_handler], 54 | ) 55 | 56 | # NOTE: This really sucks, but it seems to be impossible to get pytest to 57 | # not pre-configure the logging package, so we have to override the config 58 | # for the child logger explicitly. This is NOT necessary when running the 59 | # content of this test in a plain vanilla python session, but since pytest 60 | # does some configuration of its own before the test starts, the above 61 | # basicConfig call doesn't override that configuration. 62 | logger = logging.getLogger('mychan') 63 | logger.setLevel(logging.INFO) 64 | logger.addHandler(stream_handler) 65 | stream_handler.formatter = logging.Formatter(fmt) 66 | 67 | # Make sure debug2 works 68 | logger.debug2('Yep') 69 | assert log_stream.getvalue() == '' 70 | 71 | # Log to a standard enabled level and make sure the formatting is right 72 | # 2021-11-18 15:16:07,468 [INFO] test123 73 | logger.info('message') 74 | log_lines = list(filter(lambda x: x, log_stream.getvalue().split('\n'))) 75 | assert len(log_lines) == 1 76 | assert re.match(r'\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d \[INFO\] message', log_lines[0]) 77 | -------------------------------------------------------------------------------- /src/python/tests/test_protocols.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | """ALog unit tests to confirm that the protocol is aligned `logging.Logger`.""" 25 | 26 | # Standard 27 | from typing import List 28 | import importlib 29 | import inspect 30 | import sys 31 | 32 | # Third Party 33 | import pytest 34 | 35 | # Local 36 | from alog.protocols import LoggerProtocol 37 | 38 | 39 | def get_parameter_names(obj: object, method_name: str) -> List[str]: 40 | """ 41 | Get the parameter names for an object's method 42 | """ 43 | signature = inspect.signature(getattr(obj, method_name)) 44 | return list(signature.parameters) 45 | 46 | 47 | def _populate_constants(): 48 | """ 49 | Idempotent function to populate the "plain vanilla" logging constants 50 | without the additions from alog 51 | """ 52 | real_logging = sys.modules.pop("logging") 53 | local_logging = importlib.import_module("logging") 54 | LOGGER_FUNCTIONS = { 55 | fn[0] for fn in inspect.getmembers(local_logging.Logger, inspect.isfunction) 56 | } 57 | LOGGER_DOCSTRINGS = { 58 | fn: inspect.getdoc(getattr(local_logging.Logger, fn)) for fn in LOGGER_FUNCTIONS 59 | } 60 | LOGGER_PARAMETERS = { 61 | fn: get_parameter_names(local_logging.Logger, fn) for fn in LOGGER_FUNCTIONS 62 | } 63 | 64 | sys.modules["logging"] = real_logging 65 | 66 | return (LOGGER_FUNCTIONS, LOGGER_DOCSTRINGS, LOGGER_PARAMETERS) 67 | 68 | 69 | LOGGER_FUNCTIONS, LOGGER_DOCSTRINGS, LOGGER_PARAMETERS = _populate_constants() 70 | 71 | 72 | PROTOCOL_FUNCTIONS = { 73 | fn[0] for fn in inspect.getmembers(LoggerProtocol, inspect.isfunction) 74 | } 75 | PROTOCOL_DOCSTRINGS = { 76 | fn: inspect.getdoc(getattr(LoggerProtocol, fn)) for fn in LOGGER_FUNCTIONS 77 | } 78 | PROTOCOL_PARAMETERS = { 79 | fn: get_parameter_names(LoggerProtocol, fn) for fn in LOGGER_FUNCTIONS 80 | } 81 | 82 | 83 | def test_protocol_is_complete(): 84 | """ 85 | Test that all functions in stdlib `Logger` exists in the protocol `LoggerProtocol` 86 | """ 87 | excluded_functions = {"__reduce__", "__repr__"} 88 | 89 | fn_not_in_protocol = LOGGER_FUNCTIONS.difference(PROTOCOL_FUNCTIONS) 90 | fn_not_in_protocol = fn_not_in_protocol.difference(excluded_functions) 91 | 92 | assert not fn_not_in_protocol, "All Logger functions should exist in the protocol" 93 | 94 | 95 | def test_protocol_docstrings(): 96 | """ 97 | Test that all docstrings in `LoggerProtocol` matches those of the stdlib `Logger` 98 | """ 99 | assert PROTOCOL_DOCSTRINGS == LOGGER_DOCSTRINGS 100 | 101 | 102 | @pytest.mark.skipif(sys.version_info < (3, 8), reason="Skipping deprecated python signatures") 103 | def test_protocol_function_signatures(): 104 | """ 105 | Test that all function parameter names in `LoggerProtocol` matches those of the 106 | stdlib `Logger` 107 | """ 108 | # Adjust LOGGER_PARAMETERS for expected differences. 109 | # These are aligned with the typeshed type definition where `*kwargs` is replaced 110 | # with `exc_info`, `stack_info`, `stacklevel`, `extra` 111 | param_list = { 112 | "log", 113 | "debug", 114 | "info", 115 | "warn", 116 | "warning", 117 | "error", 118 | "critical", 119 | "fatal", 120 | } 121 | adjusted_logger_parameters = {} 122 | for param in LOGGER_PARAMETERS: 123 | params = [p for p in LOGGER_PARAMETERS[param]] 124 | if param in param_list: 125 | params.remove("kwargs") 126 | params.extend(["exc_info", "stack_info", "stacklevel", "extra"]) 127 | elif param == "exception": 128 | params.remove("kwargs") 129 | params.extend(["stack_info", "stacklevel", "extra"]) 130 | adjusted_logger_parameters[param] = params 131 | 132 | # Compare parameters against adjusted Logger parameters 133 | assert PROTOCOL_PARAMETERS == adjusted_logger_parameters 134 | 135 | 136 | # Type annotations/hints aren't tested since those are included in the stdlib `Logger` 137 | -------------------------------------------------------------------------------- /src/python/tests/test_util.py: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # MIT License 3 | # 4 | # Copyright (c) 2021 IBM 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ################################################################################ 24 | '''ALog utils unit tests. 25 | ''' 26 | 27 | import os 28 | import pytest 29 | import unittest 30 | from glob import glob 31 | 32 | from util import correct_log_codes 33 | 34 | def get_copy_file_name(filename): 35 | # Get the of an output file (if changes found) in copy mode 36 | return '{}_copy.py'.format(os.path.splitext(filename)[0]) 37 | 38 | def get_code_match(line): 39 | # Return search result for our main log code regex 40 | return correct_log_codes.LOG_PATTERN.search(line) 41 | 42 | class TestLogCorrection(unittest.TestCase): 43 | '''Ensures that printed messages are valid json format when json formatting is specified''' 44 | fixtures_dir = os.path.join(os.path.dirname(__file__), 'fixtures') 45 | duplicate_file_dir = os.path.join(fixtures_dir, 'duplicate_replacement') 46 | duplicates_file = os.path.join(duplicate_file_dir, 'test_case_1.py') 47 | no_duplicates_file = os.path.join(duplicate_file_dir, 'test_case_2.py') 48 | 49 | placholders_file_dir = os.path.join(fixtures_dir, 'placeholders') 50 | prefix_file = os.path.join(placholders_file_dir, 'test_case_3.py') 51 | prefixless_file = os.path.join(placholders_file_dir, 'test_case_4.py') 52 | prefix = '') is not None) 76 | self.assertTrue(get_code_match('') is not None) 77 | self.assertTrue(get_code_match('') is not None) 78 | self.assertTrue(get_code_match('') is not None) 79 | self.assertTrue(get_code_match('') is not None) 80 | 81 | def test_log_code_regex_negative_cases(self): 82 | '''Example negative cases for main log code regex''' 83 | # Numbers only 84 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('<12345678>') is None) 85 | # No prefix 86 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('<12345678E>') is None) 87 | # Prefix doesn't have enough letters 88 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 89 | # Prefix has too many letters 90 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 91 | # Not enough digits 92 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 93 | # Too many digits 94 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 95 | # Lowercase letters in prefix 96 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 97 | # Bad level 98 | self.assertTrue(correct_log_codes.LOG_PATTERN.search('') is None) 99 | 100 | def test_it_makes_a_file_if_duplicates_in_copy_mode(self): 101 | '''If we run in copy mode and actually find duplicates, we make a copy file with corrections''' 102 | res_file = get_copy_file_name(self.duplicates_file) 103 | self.assertTrue(os.path.isfile(res_file)) 104 | 105 | def test_it_does_not_make_a_file_if_no_duplicates_in_copy_mode(self): 106 | '''If we run in copy mode do not find duplicates, we don't make a copy file''' 107 | res_file = get_copy_file_name(self.no_duplicates_file) 108 | self.assertFalse(os.path.isfile(res_file)) 109 | 110 | def test_it_fixes_duplicates(self): 111 | '''Tests that a file of only duplicated log codes will replace all duplicates''' 112 | res_file = get_copy_file_name(self.duplicates_file) 113 | # Get the original log code lineset (anything giving a hit for our log code regex) 114 | with open(self.duplicates_file) as dup_file: 115 | original_codes = [get_code_match(x).group() for x in dup_file.readlines() if get_code_match(x)] 116 | num_codes = len(original_codes) 117 | unique_original_codes = set(original_codes) 118 | # Get the output log code lineset 119 | with open(res_file) as out_file: 120 | out_lines = set([get_code_match(x).group() for x in out_file.readlines() if get_code_match(x)]) 121 | # Duplicate file should only have one log code 122 | self.assertEqual(len(unique_original_codes), 1) 123 | # But there should be lots of occurrences of that code 124 | self.assertTrue(num_codes > 1) 125 | # And after, we should have a separate code per line 126 | self.assertEqual(len(out_lines), num_codes) 127 | 128 | def test_it_preserves_whitespace(self): 129 | '''Tests that a duplicated log codes with whitespace will keep whitespace the same''' 130 | res_file = get_copy_file_name(self.duplicates_file) 131 | with open(self.duplicates_file) as dup_file: 132 | old_matches = [get_code_match(x) for x in dup_file] 133 | with open(res_file) as out_file: 134 | new_matches = [get_code_match(x) for x in out_file] 135 | at_least_one = False 136 | for old, new in zip(old_matches, new_matches): 137 | # Matches better align, or else something is very wrong 138 | self.assertTrue((old is None and new is None) or (old is not None and new is not None)) 139 | # If we match, ensure that span boundaries align 140 | if old is not None: 141 | at_least_one = True 142 | old_start, old_end = old.span() 143 | new_start, new_end = new.span() 144 | self.assertEqual(old_start, new_start) 145 | self.assertEqual(old_end, new_end) 146 | # Make sure we actually verified span boundaries at least once 147 | self.assertTrue(at_least_one) 148 | 149 | def test_it_fills_prefixed_placeholders(self): 150 | '''Tests that it fills a prefixed placeholders correctly''' 151 | res_file = get_copy_file_name(self.prefix_file) 152 | with open(self.prefix_file) as pref_file: 153 | old_lines = pref_file.readlines() 154 | with open(res_file) as out_file: 155 | new_lines = out_file.readlines() 156 | new_matches = [get_code_match(x) for x in new_lines] 157 | at_least_one = False 158 | for idx, new in enumerate(new_matches): 159 | # Only new will match on the overall code regex 160 | if new is not None: 161 | at_least_one = True 162 | code_start, _ = new.span() 163 | # Index into corresponding line number of match & check prefixes 164 | old_prefix = old_lines[idx][code_start: code_start + 4] 165 | new_prefix = new_lines[idx][code_start: code_start + 4] 166 | self.assertEqual(old_prefix, new_prefix) 167 | # Ensure that the prefix used was not the override prefix 168 | self.assertNotEqual(new_prefix, self.prefix) 169 | # Make sure we actually verified prefixes at least once 170 | self.assertTrue(at_least_one) 171 | 172 | def test_it_fills_prefixless_placeholders(self): 173 | '''Tests that it fills prefixless placeholders with the provided prefix''' 174 | res_file = get_copy_file_name(self.prefixless_file) 175 | with open(self.prefix_file) as pref_file: 176 | old_lines = pref_file.readlines() 177 | with open(res_file) as out_file: 178 | new_lines = out_file.readlines() 179 | new_matches = [get_code_match(x) for x in new_lines] 180 | at_least_one = False 181 | for idx, new in enumerate(new_matches): 182 | # Only new will match on the overall code regex 183 | if new is not None: 184 | at_least_one = True 185 | code_start, _ = new.span() 186 | # Index into corresponding line number of match & check prefixes 187 | new_prefix = new_lines[idx][code_start: code_start + 4] 188 | # Ensure that the prefix used was the override 189 | self.assertEqual(new_prefix, self.prefix) 190 | # Make sure we actually verified prefixes at least once 191 | self.assertTrue(at_least_one) 192 | -------------------------------------------------------------------------------- /src/python/util/alog_json_converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################################################################ 3 | # MIT License 4 | # 5 | # Copyright (c) 2021 IBM 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | ################################################################################ 25 | 26 | from datetime import datetime 27 | import traceback 28 | import sys 29 | import json 30 | 31 | import alog 32 | import munch 33 | 34 | log_level = sys.argv[1] if len(sys.argv) > 1 else 'debug4' 35 | log_filters = sys.argv[2] if len(sys.argv) > 2 else '' 36 | alog.configure(log_level, log_filters) 37 | 38 | try: 39 | for line in sys.stdin: 40 | try: 41 | js = json.loads(line) 42 | 43 | # Only print it if enabled 44 | chan = alog.use_channel(js['channel']) 45 | level = js.get('level_str') 46 | if level is None: 47 | level = js.get('level') 48 | if chan.isEnabledFor(alog.alog.g_alog_name_to_level[level]): 49 | 50 | # Parse the timestamp 51 | # NOTE: DIfferent languages format their timestamps slightly differently 52 | try: 53 | timestamp = datetime.strptime(js['timestamp'], '%Y-%m-%dT%H:%M:%S.%f') 54 | except ValueError: 55 | timestamp = datetime.strptime(js['timestamp'], '%Y-%m-%dT%H:%M:%S.%fZ') 56 | 57 | # Create a munch to simulate a log record 58 | m = munch.DefaultMunch(None, js) 59 | 60 | # If there is no 'message' field in the json, add an empty one 61 | js.setdefault('message', '') 62 | 63 | # Add in the internal names of the fileds 64 | m.created = timestamp.timestamp() 65 | m.msg = js 66 | m.name = m.channel 67 | m.levelname = m.level_str 68 | if m.levelname is None: 69 | m.levelname = m.level 70 | 71 | # Set the formatter's indentation 72 | # NOTE: Handle per-thread indent implementation change 73 | if hasattr(alog.alog.AlogFormatterBase, 'ThreadLocalIndent'): 74 | alog.alog.g_alog_formatter._indent.indent = m.num_indent 75 | else: 76 | alog.alog.g_alog_formatter._indent = m.num_indent 77 | 78 | # Remove all entries that are not needed anymore 79 | for k in ['timestamp', 'channel', 'level_str', 'level', 'num_indent']: 80 | if k in js: 81 | del js[k] 82 | 83 | # Remove any keys whose value is None 84 | null_keys = [] 85 | for k, v in js.items(): 86 | if v is None: 87 | null_keys.append(k) 88 | for k in null_keys: 89 | del js[k] 90 | 91 | # Log it using the log formatter 92 | print(alog.alog.g_alog_formatter.format(m)) 93 | sys.stdout.flush() 94 | 95 | # Handle non-json lines gracefully 96 | except ValueError: 97 | print("(*) {}".format(line.strip())) 98 | 99 | # Handle bugs in the script 100 | except Exception: 101 | traceback.print_exc() 102 | 103 | except KeyboardInterrupt: 104 | sys.stdout.flush() 105 | pass 106 | -------------------------------------------------------------------------------- /src/python/util/requirements.txt: -------------------------------------------------------------------------------- 1 | munch>=2.5.0,<5 2 | -------------------------------------------------------------------------------- /src/ts/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | dist 5 | -------------------------------------------------------------------------------- /src/ts/.npmignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | TODO.md 3 | test 4 | coverage 5 | .dockerignore 6 | .nyc_output 7 | ci 8 | tslint.json 9 | -------------------------------------------------------------------------------- /src/ts/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Base ####################################################################### 2 | # 3 | # This phase sets up dependenices for the other phases 4 | ## 5 | FROM node:14-slim as base 6 | 7 | # This image is only for building, so we run as root 8 | WORKDIR /src 9 | 10 | # Install build, test, andn publish dependencies 11 | COPY *.json /src/ 12 | RUN npm install 13 | 14 | ## Test ######################################################################## 15 | # 16 | # This phase runs the unit tests for the library 17 | ## 18 | FROM base as test 19 | COPY . /src 20 | RUN npm run lint && npm test 21 | 22 | ## Release ##################################################################### 23 | # 24 | # This phase builds the release and publishes it to npm 25 | ## 26 | FROM test as release 27 | ARG NPM_TOKEN 28 | ARG TS_RELEASE_VERSION 29 | ARG RELEASE_DRY_RUN 30 | RUN true && \ 31 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \ 32 | npm whoami && \ 33 | ./ci/publish.sh && \ 34 | true 35 | 36 | ## Release Test ################################################################ 37 | # 38 | # This phase installs the indicated version from npm and runs the unit tests 39 | # against the installed version 40 | ## 41 | FROM base as release_test 42 | ARG TS_RELEASE_VERSION 43 | ARG RELEASE_DRY_RUN 44 | COPY ./test /src/test 45 | RUN true && \ 46 | ([ "$RELEASE_DRY_RUN" != "true" ] && sleep 30 || true) && \ 47 | sed -i'' 's,alchemy-logging,test-alchemy-logging,g' package.json && \ 48 | sed -i'' 's,../src/,alchemy-logging/dist/,g' test/*.ts && \ 49 | sed -i'' "s,../src',alchemy-logging',g" test/*.ts && \ 50 | npm install alchemy-logging@${TS_RELEASE_VERSION} && \ 51 | npm test && \ 52 | true 53 | -------------------------------------------------------------------------------- /src/ts/README.md: -------------------------------------------------------------------------------- 1 | # Alchemy Logging (alog) - Typescript / Javascript 2 | The `alog` framework provides tunable logging with easy-to-use defaults and power-user capabilities. The mantra of `alog` is **"Log Early And Often"**. To accomplish this goal, `alog` makes it easy to enable verbose logging at develop/debug time and trim the verbosity at production run time. 3 | 4 | ## Channels and Levels 5 | The primary components of the system are **channels** and **levels** which allow for each log statement to be enabled or disabled when appropriate. 6 | 7 | 1. **Channels**: Each logging statement is made to a specific channel. Channels are independent of one another and allow for logical grouping of log messages by functionality. A channel can be any string. 8 | 9 | 1. **Levels**: Each logging statement is made at a specific level. Levels provide sequential granularity, allowing detailed debugging statements to be placed in the code without clogging up the logs at runtime. The sequence of levels and their general usage is as follows: 10 | 11 | 1. `off`: Disable the given channel completely 12 | 1. `fatal`: A fatal error has occurred. Any behavior after this statement should be regarded as undefined. 13 | 1. `error`: An unrecoverable error has occurred. Any behavior after this statement should be regarded as undefined unless the error is explicitly handled. 14 | 1. `warning`: A recoverable error condition has come up that the service maintainer should be aware of. 15 | 1. `info`: High-level information that is valuable at runtime under moderate load. 16 | 1. `trace`: Used to log begin/end of functions for debugging code paths. 17 | 1. `debug`: High-level debugging statements such as function parameters. 18 | 1. `debug1`: High-level debugging statements. 19 | 1. `debug2`: Mid-level debugging statements such as computed values. 20 | 1. `debug3`: Low-level debugging statements such as computed values inside loops. 21 | 1. `debug4`: Ultra-low-level debugging statements such as data dumps and/or statements inside multiple nested loops. 22 | 23 | Using this combination of **Channels** and **Levels**, you can fine-tune what log statements are enabled when you run your application under different circumstances. 24 | 25 | ## Configuration 26 | There are three primary pieces of configuration when setting up the `alog` environment: 27 | 28 | 1. **defaultLevel**: This is the level that will be enabled for a given channel when a specific level has not been set in the **filters**. 29 | 30 | 1. **filters**: This is a mapping from channel name to level that allows levels to be set on a per-channel basis. 31 | 32 | 1. **formatter**: This is the type of output formatting to use. It defaults to `pretty-print` for ease of readability during development, but can also be configured to log structured `json` records for production logging opviz frameworks. 33 | 34 | The `alog.configure()` function allows the default level, filters, and formatter to be set all at once. For example: 35 | 36 | ```ts 37 | import alog from 'alchemy-logging'; 38 | 39 | // Set the default level to info with filter overrides for the HTTP and ROUTR 40 | // channels to debug and warning respectively. Configure the formatter to be 41 | // structured json. 42 | alog.configure('info', 'HTTP:debug,ROUTR:warning', 'json'); 43 | ``` 44 | 45 | There are several ways to call `configure`, depending on where the values are coming from. The above example shows how it can be called with string input which is best when reading configuration variables from the environment. If programmatically setting the values, the native configuration values can be used as well. Here are some examples: 46 | 47 | ```ts 48 | // Use the native level value and map syntax 49 | alog.configure(alog.INFO, {HTTP: alog.DEBUG, ROUTR: alog.WARNING}); 50 | 51 | // Use a configure object 52 | alog.configure({ 53 | defaultLevel: alog.INFO, 54 | filters: { 55 | HTTP: alog.DEBUG, 56 | ROUTR: alog.WARNING, 57 | }, 58 | formatter: alog.PrettyFormatter, 59 | }) 60 | ``` 61 | 62 | ### Custom Formatters 63 | 64 | For finer-grained control over the formatting of the log records, you can provide a custom formatter function. It must conform to the signature: 65 | 66 | ```ts 67 | export type FormatterFunc = (logRecord: alog.LogRecord) => string; 68 | ``` 69 | 70 | The most common custom formatter ask is for a version of the `PrettyFormatter` with a different channel-string truncation level. This can be easily achieved with a wrapper around the standard `PrettyFormatter`: 71 | 72 | ```ts 73 | alog.configure({ 74 | defaultLevel: alog.INFO, 75 | formatter: (record: alog.LogRecord): string => alog.PrettyFormatter(record, 12), 76 | }); 77 | ``` 78 | 79 | ### Custom Output Streams 80 | 81 | By default `alog` will always log to `process.stdout`. If you need to capture the formatted output log (for example in a log file), you can use `alog.addOutputStream`: 82 | 83 | ```ts 84 | import alog from 'alchemy-logging'; 85 | import { createWriteStream } from 'stream'; 86 | 87 | alog.configure('debug', '', 'json'); 88 | 89 | // Add the custom stream to the output file 90 | const logFileStream = createWriteStream('output.json'); 91 | alog.addOutputStream(logFileStream); 92 | ``` 93 | 94 | ## Logging 95 | 96 | Now that the framework has been configured, it's time to actually make log entries! 97 | 98 | ### Top-Level Log Functions 99 | 100 | The simplest way to log information is to use the top-level log functions. For example: 101 | 102 | ```ts 103 | alog.info('CHANL', 'Hello from alog!'); 104 | ``` 105 | 106 | All log functions have several required and optional components: 107 | 108 | * `channel` (required): The first argument is always the channel. 109 | * `logCode` (optional): If desired, a `logCode` can be given as the second argument. A log code is a unique string enclosed in `<>` which will identify a specific log record for easy retrieval later. For example: 110 | 111 | ```ts 112 | alog.info('CHANL', '', 'This is a log that we want to be able to look up in prod'); 113 | ``` 114 | 115 | * `message` (required): The `message` argument can be either a string or a generator function that takes no arguments and lazily creates the log message. A generator function can be useful when the string is expensive to construct. For example: 116 | 117 | ```ts 118 | alog.debug4('CHANL', () => { 119 | let message: string = '[ '; 120 | for (const entry of bigArray) { 121 | string += `${entry} `; 122 | } 123 | message += ']'; 124 | return message; 125 | }); 126 | ``` 127 | 128 | * `metadata` (optional): A map of JSON-serializable `metadata` can be added as the last argument to any log function. For example: 129 | 130 | ```ts 131 | try { 132 | thisIsNotGoingToWork(); 133 | } catch (e) { 134 | alog.warning('CHANL', 'Something is wrong, but life goes on!', { 135 | error: { 136 | message: e.message, 137 | filename: e.fileName, 138 | line_number: e.lineNumber, 139 | }, 140 | }); 141 | } 142 | ``` 143 | 144 | ### Channel Logs 145 | 146 | It's often desirable to bind a shared channel name to an object so that it can be reused across a portion of your codebase. To accomplish this, `alog` supports the `useChannel` function which creates a `ChannelLog`. The `ChannelLog` object has a function for each of the log levels which omits the first `channel` argument and instead passes the bound channel name. 147 | 148 | ```ts 149 | const channel: alog.ChannelLog = alog.useChannel('CHANL'); 150 | channel.debug2('Some details'); 151 | ``` 152 | 153 | Additionally, a `ChannelLog` supports the `isEnabled` function: 154 | 155 | ```ts 156 | const channel: alog.ChannelLog = alog.useChannel('CHANL'); 157 | if (channel.isEnabled(alog.debug2)) { 158 | const someNonFunctionalThing = makeLoggingSideEffect(); 159 | channel.debug2(`The side effect is: ${someNonFunctionalThing}`); 160 | } 161 | ``` 162 | 163 | **NOTE**: For complex log creation, the recommended method is to use a `MessageGenerator` to create the message lazily. 164 | 165 | ### Lazy Logging 166 | 167 | The `alog` framework is designed to enable low-level logging without incurring performance hits. To support this, you can use lazy log creation to only perform message creation if the given channel and level are enabled. This can be done by creating a `MessageGenerator`. A `MessageGenerator` is a function that takes no arguments and produces a `string`. For example: 168 | 169 | ```ts 170 | function expensiveStringCreation() { 171 | ... 172 | return someString; 173 | } 174 | alog.info('CHANL', expensiveStringCreation); 175 | ``` 176 | 177 | In this example, the `expensiveStringCreation` function will only be invoked if the `CHANL` channel is enabled at the `info` level. 178 | 179 | The most common expensive log creation is done with standard [Template Literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). While expanding a template literal is not terribly expensive for high-level logs, if logs are added to low-level functions, they can add up. The `alog.fmt` function acts as a [Tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) to keep common template literal syntax while leveraging lazy `MessageGenerator`s. For example: 180 | 181 | ```ts 182 | largeArray.forEach((element: any) => { 183 | alog.debug4('CHANL', alog.fmt`The element is ${element}`); 184 | }); 185 | ``` 186 | 187 | ## Utilities 188 | 189 | ### Global Metadata 190 | 191 | Sometimes there are additional pieces of information that you want attached to all log entries (e.g. an env-based deployment identifier). This is supported via the `alog.addMetadata` and `alog.removeMetadata` functions. For example: 192 | 193 | ```ts 194 | import alog from 'alchemy-logging'; 195 | alog.configure('debug'); 196 | 197 | if (process.env.DEPLOYMENT_ID) { 198 | alog.addMetadata('deployment_id', process.env.DEPLOYMENT_ID); 199 | } 200 | ``` 201 | 202 | ### Indentation 203 | 204 | To support the the mission of making logs that are easy to read at dev-time, `alog` supports the notion of `indentation` which allows the `PrettyFormatter` to display nested logs with easy-to-read indentation. This can be performed manually using `alog.indent` and `alog.deindent`. For example: 205 | 206 | ```ts 207 | import alog from 'alchemy-logging'; 208 | alog.configure('debug3'); 209 | 210 | function doit(arrayOfStuff) { 211 | alog.debug('CHANL', 'Doing it!'); 212 | alog.indent(); 213 | arrayOfStuff.forEach((entry) => { 214 | alog.debug3('CHANL', `Found entry ${entry}`); 215 | }); 216 | alog.deindent(); 217 | alog.debug('CHANL', 'Done with it'); 218 | } 219 | 220 | doit([1, 2, 3, 4]); 221 | ``` 222 | 223 | The resulting output looks like 224 | 225 | ``` 226 | 2019-11-26T18:34:15.488Z [CHANL:DBUG] Doing it! 227 | 2019-11-26T18:34:15.490Z [CHANL:DBG3] Found entry 1 228 | 2019-11-26T18:34:15.490Z [CHANL:DBG3] Found entry 2 229 | 2019-11-26T18:34:15.491Z [CHANL:DBG3] Found entry 3 230 | 2019-11-26T18:34:15.491Z [CHANL:DBG3] Found entry 4 231 | 2019-11-26T18:34:15.491Z [CHANL:DBUG] Done with it 232 | ``` 233 | -------------------------------------------------------------------------------- /src/ts/TODO.md: -------------------------------------------------------------------------------- 1 | ## MUST HAVE 2 | - [X] Full set of levels 3 | - [X] Multi-channel level setting 4 | - [X] Pretty formatting 5 | - [X] JSON formatting 6 | - [X] Lazy message construction 7 | - [X] (core detail) custom output stream, mostly for testing 8 | - [ ] Use channel 9 | 10 | ## CORE FEATURES 11 | - [ ] channel.with_metadata() 12 | - [ ] Scoped indentation 13 | - [ ] Function trace BEGIN/END 14 | - [ ] Some way to produce complex logs lazily 15 | 16 | ## NICE FEATURES 17 | - [ ] Scoped timing 18 | - [ ] Decorators for scope behavior 19 | - [ ] ALOG_USE_CHANNEL for classes 20 | -------------------------------------------------------------------------------- /src/ts/ci/develop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the cpp root 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Build the base as the develop shell 7 | docker build . --target=base -t alog-ts-develop 8 | 9 | # Run with source mounted 10 | docker run --rm -it -v $PWD:/src --entrypoint /bin/bash -w /src alog-ts-develop 11 | -------------------------------------------------------------------------------- /src/ts/ci/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run from the base of the ts directory 4 | cd $(dirname ${BASH_SOURCE[0]})/.. 5 | 6 | # Make sure NPM_TOKEN is set 7 | if [ "$NPM_TOKEN" == "" ] 8 | then 9 | echo "Must set NPM_TOKEN" 10 | exit 1 11 | fi 12 | 13 | # Make sure the provided version matches the version in package.json 14 | if [ "$TS_RELEASE_VERSION" == "" ] 15 | then 16 | echo "Must specify TS_RELEASE_VERSION" 17 | exit 1 18 | fi 19 | version_placeholder="0.0.0-REPLACEME" 20 | if [[ "$OSTYPE" == "darwin"* ]] 21 | then 22 | sed_cmd="sed -i .bak" 23 | else 24 | sed_cmd="sed -i.bak" 25 | fi 26 | $sed_cmd "s,$version_placeholder,$TS_RELEASE_VERSION,g" package.json 27 | rm package.json.bak 28 | 29 | # If this is a dry run, add the flag 30 | dry_run_flag="" 31 | if [ "$RELEASE_DRY_RUN" == "true" ] 32 | then 33 | dry_run_flag="--dry-run" 34 | fi 35 | 36 | # Run the build 37 | npm run build 38 | 39 | # Run publish 40 | npm whoami 41 | npm publish $dry_run_flag 42 | 43 | # Replace the placeholder 44 | $sed_cmd "s,$TS_RELEASE_VERSION,$version_placeholder,g" package.json 45 | rm package.json.bak 46 | -------------------------------------------------------------------------------- /src/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alchemy-logging", 3 | "version": "0.0.0-REPLACEME", 4 | "description": "Alchemy Logging implementation in NodeJS/TypeScript", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "NODE_ENV=test nyc mocha --exit --require ts-node/register 'test/**/*.ts'", 9 | "lint": "./node_modules/tslint/bin/tslint --project . --config ./tslint.json", 10 | "clean": "rm -rf dist", 11 | "build": "tsc", 12 | "prepare": "npm run build", 13 | "repl": "ts-node" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:IBM/alchemy-logging.git" 18 | }, 19 | "nyc": { 20 | "extension": [ 21 | ".ts", 22 | ".tsx" 23 | ], 24 | "exclude": [ 25 | "**/*.d.ts", 26 | "coverage/**", 27 | "dist/**", 28 | "test/**" 29 | ], 30 | "reporter": [ 31 | "text-summary", 32 | "html" 33 | ], 34 | "all": true 35 | }, 36 | "author": "Gabe Goodhart", 37 | "license": "MIT", 38 | "dependencies": { 39 | "@types/node": "^22.0.0", 40 | "deepcopy": "^2.0.0", 41 | "typescript": "^5.7.3" 42 | }, 43 | "devDependencies": { 44 | "@types/chai": "^4.2.4", 45 | "@types/mocha": "^10.0.0", 46 | "chai": "^4.2.0", 47 | "deep-equal": "^2.0.5", 48 | "memory-streams": "^0.1.3", 49 | "mocha": "^11.1.0", 50 | "nyc": "^17.0.0", 51 | "rewire": "^7.0.0", 52 | "ts-node": "^10.2.0", 53 | "tslint": "^6.0.0-beta0", 54 | "tslint-eslint-rules": "^5.4.0", 55 | "tslint-no-unused-expression-chai": "^0.1.4" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ts/src/alog.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | // Standard 26 | import { Writable } from 'stream'; 27 | 28 | // Local 29 | import { AlogCoreSingleton } from './core'; 30 | 31 | // Pass-Through exports 32 | export { ChannelLog, useChannel } from './channel-log'; 33 | export { configure } from './configure'; 34 | export { JsonFormatter, PrettyFormatter, fmt } from './formatters'; 35 | export { 36 | AlogConfig, 37 | AlogConfigError, 38 | DEBUG, 39 | DEBUG1, 40 | DEBUG2, 41 | DEBUG3, 42 | DEBUG4, 43 | ERROR, 44 | FATAL, 45 | FilterMap, 46 | FormatterFunc, 47 | INFO, 48 | LogMetadata, 49 | LogRecord, 50 | MessageGenerator, 51 | OFF, 52 | TRACE, 53 | WARNING, 54 | } from './types'; 55 | 56 | // Exports ///////////////////////////////////////////////////////////////////// 57 | 58 | // Expose parts of the singleton directly 59 | export const addMetadata = AlogCoreSingleton.getInstance().addMetadata; 60 | export const addOutputStream = AlogCoreSingleton.getInstance().addOutputStream; 61 | export const deindent = AlogCoreSingleton.getInstance().deindent; 62 | export const indent = AlogCoreSingleton.getInstance().indent; 63 | export const isEnabled = AlogCoreSingleton.getInstance().isEnabled; 64 | export const removeMetadata = AlogCoreSingleton.getInstance().removeMetadata; 65 | export const resetOutputStreams = AlogCoreSingleton.getInstance().resetOutputStreams; 66 | 67 | // Add all of the level functions 68 | export const fatal = AlogCoreSingleton.getInstance().fatal; 69 | export const error = AlogCoreSingleton.getInstance().error; 70 | export const warning = AlogCoreSingleton.getInstance().warning; 71 | export const info = AlogCoreSingleton.getInstance().info; 72 | export const trace = AlogCoreSingleton.getInstance().trace; 73 | export const debug = AlogCoreSingleton.getInstance().debug; 74 | export const debug1 = AlogCoreSingleton.getInstance().debug1; 75 | export const debug2 = AlogCoreSingleton.getInstance().debug2; 76 | export const debug3 = AlogCoreSingleton.getInstance().debug3; 77 | export const debug4 = AlogCoreSingleton.getInstance().debug4; 78 | -------------------------------------------------------------------------------- /src/ts/src/channel-log.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | // Local 26 | import { AlogCoreSingleton } from './core'; 27 | import { LogCode, Loggable, LogMetadata } from './types'; 28 | 29 | /** 30 | * The ChannelLog object binds a channel name and wraps all of the core 31 | * functions which are scoped to an individual channel. 32 | */ 33 | export class ChannelLog { 34 | 35 | public channel: string; 36 | private coreInstance: AlogCoreSingleton; 37 | 38 | constructor(channel: string) { 39 | this.channel = channel; 40 | this.coreInstance = AlogCoreSingleton.getInstance(); 41 | } 42 | 43 | /*-- Log Functions --*/ 44 | 45 | // Log to fatal 46 | public fatal(message: Loggable, metadata?: LogMetadata): void; 47 | public fatal(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 48 | public fatal( 49 | codeOrMsg: LogCode|Loggable, 50 | msgOrMeta?: Loggable|LogMetadata, 51 | meta?: LogMetadata, 52 | ) { 53 | AlogCoreSingleton.getInstance().fatal( 54 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 55 | ); 56 | } 57 | 58 | // Log to error 59 | public error(message: Loggable, metadata?: LogMetadata): void; 60 | public error(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 61 | public error( 62 | codeOrMsg: LogCode|Loggable, 63 | msgOrMeta?: Loggable|LogMetadata, 64 | meta?: LogMetadata, 65 | ) { 66 | AlogCoreSingleton.getInstance().error( 67 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 68 | ); 69 | } 70 | 71 | // Log to warning 72 | public warning(message: Loggable, metadata?: LogMetadata): void; 73 | public warning(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 74 | public warning( 75 | codeOrMsg: LogCode|Loggable, 76 | msgOrMeta?: Loggable|LogMetadata, 77 | meta?: LogMetadata, 78 | ) { 79 | AlogCoreSingleton.getInstance().warning( 80 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 81 | ); 82 | } 83 | 84 | // Log to info 85 | public info(message: Loggable, metadata?: LogMetadata): void; 86 | public info(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 87 | public info( 88 | codeOrMsg: LogCode|Loggable, 89 | msgOrMeta?: Loggable|LogMetadata, 90 | meta?: LogMetadata, 91 | ) { 92 | AlogCoreSingleton.getInstance().info( 93 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 94 | ); 95 | } 96 | 97 | // Log to trace 98 | public trace(message: Loggable, metadata?: LogMetadata): void; 99 | public trace(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 100 | public trace( 101 | codeOrMsg: LogCode|Loggable, 102 | msgOrMeta?: Loggable|LogMetadata, 103 | meta?: LogMetadata, 104 | ) { 105 | AlogCoreSingleton.getInstance().trace( 106 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 107 | ); 108 | } 109 | 110 | // Log to debug 111 | public debug(message: Loggable, metadata?: LogMetadata): void; 112 | public debug(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 113 | public debug( 114 | codeOrMsg: LogCode|Loggable, 115 | msgOrMeta?: Loggable|LogMetadata, 116 | meta?: LogMetadata, 117 | ) { 118 | AlogCoreSingleton.getInstance().debug( 119 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 120 | ); 121 | } 122 | 123 | // Log to debug1 124 | public debug1(message: Loggable, metadata?: LogMetadata): void; 125 | public debug1(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 126 | public debug1( 127 | codeOrMsg: LogCode|Loggable, 128 | msgOrMeta?: Loggable|LogMetadata, 129 | meta?: LogMetadata, 130 | ) { 131 | AlogCoreSingleton.getInstance().debug1( 132 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 133 | ); 134 | } 135 | 136 | // Log to debug2 137 | public debug2(message: Loggable, metadata?: LogMetadata): void; 138 | public debug2(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 139 | public debug2( 140 | codeOrMsg: LogCode|Loggable, 141 | msgOrMeta?: Loggable|LogMetadata, 142 | meta?: LogMetadata, 143 | ) { 144 | AlogCoreSingleton.getInstance().debug2( 145 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 146 | ); 147 | } 148 | 149 | // Log to debug3 150 | public debug3(message: Loggable, metadata?: LogMetadata): void; 151 | public debug3(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 152 | public debug3( 153 | codeOrMsg: LogCode|Loggable, 154 | msgOrMeta?: Loggable|LogMetadata, 155 | meta?: LogMetadata, 156 | ) { 157 | AlogCoreSingleton.getInstance().debug3( 158 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 159 | ); 160 | } 161 | 162 | // Log to debug4 163 | public debug4(message: Loggable, metadata?: LogMetadata): void; 164 | public debug4(logCode: LogCode, message?: Loggable, metadata?: LogMetadata): void; 165 | public debug4( 166 | codeOrMsg: LogCode|Loggable, 167 | msgOrMeta?: Loggable|LogMetadata, 168 | meta?: LogMetadata, 169 | ) { 170 | AlogCoreSingleton.getInstance().debug4( 171 | this.channel, codeOrMsg as string, msgOrMeta as Loggable, meta, 172 | ); 173 | } 174 | 175 | /*-- Helper Functions --*/ 176 | 177 | public isEnabled(level: number): boolean { 178 | return this.coreInstance.isEnabled(this.channel, level); 179 | } 180 | }; 181 | 182 | /** @brief Factory wrapper for creating a new channel */ 183 | export function useChannel(channel: string): ChannelLog { 184 | return new ChannelLog(channel); 185 | } 186 | -------------------------------------------------------------------------------- /src/ts/src/configure.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | // Local 26 | import { AlogCoreSingleton } from './core'; 27 | import { defaultFormatterMap } from './formatters'; 28 | import { 29 | AlogConfig, 30 | AlogConfigError, 31 | FilterMap, 32 | FormatterFunc, 33 | OFF, 34 | } from './types'; 35 | 36 | // Validation ////////////////////////////////////////////////////////////////// 37 | 38 | function isValidLevel(lvl: any) { 39 | if (typeof lvl === 'number') { 40 | return Number.isInteger(lvl) && lvl > 0; 41 | } else if (typeof lvl === 'string') { 42 | return Object.keys(AlogCoreSingleton.levelFromName).includes(lvl); 43 | } else { 44 | return false; 45 | } 46 | } 47 | 48 | function isValidFilterConfig(filterConfig: any) { 49 | return Object.keys(filterConfig).reduce((obj: {[key: string]: boolean}, key: string) => { 50 | obj.isValid = obj.isValid && isValidLevel(filterConfig[key]); 51 | return obj; 52 | }, {isValid: true}).isValid; 53 | } 54 | 55 | function isValidConfig(configObject: any): boolean { 56 | 57 | // defaultLevel 58 | if (!isValidLevel(configObject.defaultLevel)) { 59 | return false; 60 | } 61 | 62 | // filters 63 | if (configObject.filters !== undefined) { 64 | if (typeof configObject.filters !== 'object') { 65 | return false; 66 | } 67 | if (!isValidFilterConfig(configObject.filters)) { 68 | return false; 69 | } 70 | } 71 | 72 | // formatter 73 | if (configObject.formatter !== undefined) { 74 | let validFormatter = false; 75 | if (typeof configObject.formatter === 'function') { 76 | validFormatter = true; 77 | } else if (typeof configObject.formatter === 'string' 78 | && Object.keys(defaultFormatterMap).includes(configObject.formatter)) { 79 | validFormatter = true; 80 | } 81 | if (!validFormatter) { 82 | return false; 83 | } 84 | } 85 | 86 | return true; 87 | } 88 | 89 | // Configure Implementation //////////////////////////////////////////////////// 90 | 91 | function levelFromArg(level: string | number): number { 92 | if (typeof level === 'string') { 93 | if (isValidLevel(level)) { 94 | return AlogCoreSingleton.levelFromName[level]; 95 | } 96 | throw new AlogConfigError(`Invalid level name: [${level}]`); 97 | } else if (typeof level === 'number') { 98 | if (isValidLevel(level)) { 99 | return level; 100 | } 101 | throw new AlogConfigError(`Invalid level number: [${level}]`); 102 | } else { 103 | throw new AlogConfigError(`Invalid argument type: [${typeof level}]`); 104 | } 105 | } 106 | 107 | function filtersFromArg(filters: FilterMap | string): FilterMap { 108 | 109 | if (filters === null || filters === undefined) { 110 | return {}; 111 | } else if (typeof filters === 'object') { 112 | if (!isValidFilterConfig(filters)) { 113 | throw new AlogConfigError(`Invalid filter config: ${JSON.stringify(filters)}`); 114 | } 115 | return filters; 116 | } else if (typeof filters === 'string') { 117 | // Parse if it's a string 118 | const parsed: any = {}; 119 | filters.split(',').filter((part) => part.trim() !== '').forEach((part: string) => { 120 | const keyVal = part.split(':'); 121 | if (keyVal.length !== 2) { 122 | throw new AlogConfigError(`Invalid filter spec part [${part}]`); 123 | } else if (!isValidLevel(keyVal[1])) { 124 | throw new AlogConfigError(`Invalid filter level [${part}]`); 125 | } 126 | parsed[keyVal[0]] = AlogCoreSingleton.levelFromName[keyVal[1]]; 127 | }); 128 | return parsed; 129 | } 130 | throw new AlogConfigError(`Invalid argument type for filters: [${typeof filters}]`); 131 | } 132 | 133 | function formatterFromArg(formatter: FormatterFunc | string) { 134 | if (typeof formatter === 'function') { 135 | // If it's a function, just use it (and hope it's a valid one!) 136 | return formatter; 137 | } else if (typeof formatter === 'string') { 138 | // Otherwise, look up in the default formatter map 139 | if (defaultFormatterMap[formatter] === undefined) { 140 | throw new AlogConfigError( 141 | `Invalid formatter type "${formatter}". Options are: [${Object.keys(defaultFormatterMap)}]`); 142 | } 143 | return defaultFormatterMap[formatter]; 144 | } 145 | throw new AlogConfigError(`Invalid formatter argument type: ${typeof formatter}`); 146 | } 147 | 148 | function parseConfigureArgs( 149 | argOne: AlogConfig | string | number, 150 | filters?: FilterMap| string, 151 | formatter?: string | FormatterFunc): AlogConfig { 152 | 153 | // These are the three core pieces of config we need from the various args 154 | let parsedDefaultLevel: number = OFF; 155 | let parsedFilters: { [channelName: string]: number } = {}; 156 | let parsedFormatter: FormatterFunc = defaultFormatterMap.pretty; 157 | 158 | // argOne might be a big map of all the things: (defaultLevel, Filters, formatter) 159 | if (typeof argOne === 'object') { 160 | if (isValidConfig(argOne)) { 161 | 162 | // If other arguments specified, throw an error 163 | if (filters !== undefined || formatter !== undefined) { 164 | throw new AlogConfigError('Cannot configure with an object and explicit filter/formatter args'); 165 | } else { 166 | parsedDefaultLevel = argOne.defaultLevel; 167 | parsedFilters = argOne.filters || parsedFilters; 168 | 169 | // If formatter is a string, look it up 170 | parsedFormatter = ( 171 | (typeof argOne.formatter === 'string' && defaultFormatterMap[argOne.formatter]) 172 | || argOne.formatter) 173 | || parsedFormatter; 174 | 175 | // Return the config directly with defaults 176 | return { 177 | defaultLevel: parsedDefaultLevel, 178 | filters: parsedFilters, 179 | formatter: parsedFormatter, 180 | }; 181 | } 182 | } 183 | throw new AlogConfigError(`Invalid config object: ${JSON.stringify(argOne)}`); 184 | } else { 185 | parsedDefaultLevel = levelFromArg(argOne); 186 | } 187 | 188 | // filters 189 | if (filters !== undefined) { 190 | parsedFilters = filtersFromArg(filters); 191 | } 192 | 193 | // formatter 194 | if (formatter !== undefined) { 195 | parsedFormatter = formatterFromArg(formatter); 196 | } 197 | 198 | // Return the config 199 | return { 200 | defaultLevel: parsedDefaultLevel, 201 | filters: parsedFilters, 202 | formatter: parsedFormatter, 203 | }; 204 | } 205 | 206 | // Main configure ////////////////////////////////////////////////////////////// 207 | 208 | /** @brief Configure the core singleton and set the config for which are 209 | * enabled/disabled 210 | * 211 | * This is the primary setup function for alog. It must be called before any of 212 | * the other alog functions are called, but it may be re-invoked at any time to 213 | * update the configuration. 214 | * 215 | * @param argOne: This argument can either be a full AlogConfig object, or the 216 | * string name for the defaultLevel 217 | * @param filters: This argument can either be a map from channel name (string) 218 | * to configured level (number) or a string in the format 219 | * 'CHAN1:level,CHAN2:level' that will be parsed into the corresponding map 220 | * @param formatter: This argument can either be a formatter function, or the 221 | * string name of a predefined formatter ('pretty' or 'json') 222 | */ 223 | export function configure( 224 | argOne: AlogConfig | string | number, 225 | filters?: FilterMap | string, 226 | formatter?: string | FormatterFunc) { 227 | 228 | // Configure core singleton to support alog channels and levels 229 | const instance: AlogCoreSingleton = AlogCoreSingleton.getInstance(); 230 | 231 | // Set the current defaults 232 | const config: AlogConfig = parseConfigureArgs(argOne, filters, formatter); 233 | instance.setDefaultLevel(config.defaultLevel); 234 | instance.setFilters(config.filters); 235 | instance.setFormatter(config.formatter); 236 | } 237 | -------------------------------------------------------------------------------- /src/ts/src/formatters.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | // Local 26 | import { 27 | DEBUG, 28 | DEBUG1, 29 | DEBUG2, 30 | DEBUG3, 31 | DEBUG4, 32 | ERROR, 33 | FATAL, 34 | FormatterFunc, 35 | INFO, 36 | MessageGenerator, 37 | TRACE, 38 | WARNING, 39 | } from './types'; 40 | 41 | //////////////////////////////////////////////////////////////////////////////// 42 | // Formatter functions take in a record (by reference) and produce a string 43 | // representation. Note that the record MAY be modified in place as the core 44 | // will guarantee that it is a clean copy of any/all data used to construct it. 45 | //////////////////////////////////////////////////////////////////////////////// 46 | 47 | // Map from level to 4-character string for pretty-print header 48 | const prettyLevelNames: {[key: number]: string} = { 49 | [FATAL]: "FATL", 50 | [ERROR]: "ERRR", 51 | [WARNING]: "WARN", 52 | [INFO]: "INFO", 53 | [TRACE]: "TRCE", 54 | [DEBUG]: "DBUG", 55 | [DEBUG1]: "DBG1", 56 | [DEBUG2]: "DBG2", 57 | [DEBUG3]: "DBG3", 58 | [DEBUG4]: "DBG4", 59 | } 60 | 61 | // The pretty-print representation of an indentation 62 | const prettyIndentation = ' '; 63 | 64 | export function PrettyFormatter(record: any, channelLength: number = 5): string { 65 | 66 | //// Make the header //// 67 | let header: string = ''; 68 | 69 | // Timestamp 70 | header += record.timestamp; 71 | 72 | // Channel 73 | const chan: string = record.channel.substring(0, channelLength) 74 | + ' '.repeat(Math.max(0, channelLength - record.channel.length)); 75 | header += ` [${chan}`; 76 | 77 | // Level 78 | header += `:${prettyLevelNames[record.level] || 'UNKN'}]`; 79 | 80 | // Log Code 81 | if (record.log_code !== undefined) { 82 | header += ` ${record.log_code}`; 83 | } 84 | 85 | // Indent 86 | header += prettyIndentation.repeat(record.num_indent); 87 | 88 | //// Log each line in the message //// 89 | let outStr = ''; 90 | if (record.message) { 91 | for (const line of record.message.split('\n')) { 92 | outStr += `${header} ${line}\n`; 93 | } 94 | } 95 | 96 | //// Add Metadata //// 97 | 98 | if (record.metadata !== undefined) { 99 | for (const key of Object.keys(record.metadata)) { 100 | const val: string = JSON.stringify(record.metadata[key]); 101 | outStr += `${header} * ${key}: ${val}\n`; 102 | } 103 | } 104 | 105 | //// Add Stack Trace //// 106 | if (record.stack !== undefined) { 107 | const stackLines = record.stack.split('\n'); 108 | for (const stackLine of stackLines.slice(1)) { 109 | outStr += `${header} ${stackLine}\n`; 110 | } 111 | } 112 | 113 | // Strip off the final newline and return 114 | return outStr.trimRight(); 115 | } 116 | 117 | export function JsonFormatter(record: any): string { 118 | // Flatten the metadata into the record and serialize 119 | if (record.metadata !== undefined) { 120 | const metadata: any = record.metadata; 121 | delete record.metadata; 122 | return JSON.stringify(Object.assign(metadata, record)); 123 | } else { 124 | return JSON.stringify(record); 125 | } 126 | } 127 | 128 | export const defaultFormatterMap: {[key: string]: FormatterFunc} = { 129 | pretty: PrettyFormatter, 130 | json: JsonFormatter, 131 | } 132 | 133 | /** @brief The fmt function is a tagged template that lazily creates a message 134 | * generator 135 | * 136 | * This function is syntatic sugar around creating a MessageGenerator to perform 137 | * lazy logging. It takes advantage of the Tagged Template feature of node to 138 | * act as a Templat Literal Tag. For example: 139 | * 140 | * const val: number = 1; 141 | * alog.debug('CHAN', alog.fmt`The value is ${val}`); 142 | * 143 | * Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates 144 | */ 145 | export function fmt(template: TemplateStringsArray, ...substitutions: any[]): MessageGenerator { 146 | return () => String.raw(template, ...substitutions); 147 | } 148 | -------------------------------------------------------------------------------- /src/ts/src/index.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | import * as alog from './alog'; 26 | 27 | // Expose the alog module as the default to support 28 | // import alog from 'alchemy-logging'; 29 | export default alog; 30 | 31 | // Expose the keys directly to allow for destructuring and the old-style 32 | // import * as alog from 'alchey-logging'; 33 | Object.entries(alog).forEach((entry: [string, any]): void => { 34 | const key: string = entry[0]; 35 | const val: any = entry[1]; 36 | if (key !== '__esModule') { 37 | module.exports[key] = val; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/ts/src/types.ts: -------------------------------------------------------------------------------- 1 | /*------------------------------------------------------------------------------ 2 | * MIT License 3 | * 4 | * Copyright (c) 2021 IBM 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | *----------------------------------------------------------------------------*/ 24 | 25 | 26 | // Map type for channel -> level filters 27 | export interface FilterMap {[channel: string]: number} 28 | 29 | // Artibrary metadata dict 30 | export interface LogMetadata {[key: string]: any} 31 | 32 | // An alog record can be any arbitrary key/val map 33 | export interface LogRecord { 34 | // Required fields 35 | channel: string; 36 | level: number; 37 | level_str: string; 38 | timestamp: string; 39 | message: string; 40 | num_indent: number; 41 | 42 | // Specific optional fields 43 | log_code?: string; 44 | metadata?: LogMetadata; 45 | stack?: string; 46 | } 47 | 48 | // The type for a custom formatter function 49 | export type FormatterFunc = (logRecord: LogRecord) => string; 50 | 51 | // The type for a lazy record generator 52 | export type MessageGenerator = () => string; 53 | 54 | // The type used for a log code 55 | export type LogCode = string; 56 | 57 | // The type that defines something which can be logged 58 | export type Loggable = string|MessageGenerator|Error|any; 59 | 60 | export interface AlogConfig { 61 | defaultLevel: number; 62 | filters?: FilterMap; 63 | formatter?: FormatterFunc; 64 | } 65 | 66 | // Error raised when configuration error occurs 67 | export class AlogConfigError extends Error { 68 | constructor(message: string) { 69 | super(message); 70 | this.name = 'AlogConfigError'; 71 | } 72 | }; 73 | 74 | // The core levels and their numeric equivalents 75 | export const OFF: number = 60; 76 | export const FATAL: number = 59; 77 | export const ERROR: number = 50; 78 | export const WARNING: number = 40; 79 | export const INFO: number = 30; 80 | export const TRACE: number = 15; 81 | export const DEBUG: number = 10; 82 | export const DEBUG1: number = 9; 83 | export const DEBUG2: number = 8; 84 | export const DEBUG3: number = 7; 85 | export const DEBUG4: number = 6; 86 | -------------------------------------------------------------------------------- /src/ts/test/helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | // Standard 3 | import { Writable } from 'stream'; 4 | 5 | // Third Party 6 | import MemoryStreams from 'memory-streams'; 7 | const deepCopy = require('deepcopy'); 8 | const deepEqual = require('deep-equal'); 9 | 10 | // Alog guts 11 | import alog from '../src'; 12 | import { AlogCoreSingleton } from '../src/core'; 13 | const formatters = require('rewire')('../src/formatters'); 14 | const prettyLevelNames = formatters.__get__('prettyLevelNames'); 15 | const prettyIndentation = formatters.__get__('prettyIndentation'); 16 | 17 | const levelFromName = AlogCoreSingleton.levelFromName; 18 | const nameFromLevel = AlogCoreSingleton.nameFromLevel; 19 | 20 | /*-- Constants ---------------------------------------------------------------*/ 21 | 22 | // Sample log code so that the check_unique_log_codes script doesn't pick up the 23 | // same value in multiple places 24 | export const sampleLogCode = ''; 25 | 26 | // Sentinel string for validation functions to just check for the presence of a 27 | // given key 28 | export const IS_PRESENT = '__is_present__'; 29 | 30 | /*-- Helpers -----------------------------------------------------------------*/ 31 | 32 | // How do you log errors when testing a logger?? 33 | export function testFailureLog(msg: string): void { 34 | process.stderr.write(`** ${msg} \n`); 35 | } 36 | 37 | // Helper that validates an indivitual log record against expected values. 38 | // If the value in the expected record is IS_PRESENT, then it just checks 39 | // for the presence of the field, otherwise, it checks for equality. 40 | export function validateLogRecord(record: any, expected: any): boolean { 41 | 42 | let res = true; 43 | 44 | // Validate all expected keys 45 | for (const expKey of Object.keys(expected)) { 46 | const expValue = expected[expKey]; 47 | if (expValue === IS_PRESENT) { 48 | if (!record.hasOwnProperty(expKey)) { 49 | testFailureLog(`Missing expected key ${expKey}`); 50 | res = false; 51 | } 52 | } else if (expKey === 'metadata' && ! deepEqual(expValue, record.metadata)) { 53 | testFailureLog( 54 | `Record mismatch [metadata]. Exp: ${JSON.stringify(expValue)}, Got: ${JSON.stringify(record.metadata)}`); 55 | res = false; 56 | } else if (expKey !== 'metadata' && record[expKey] !== expValue) { 57 | testFailureLog(`Record mismatch [${expKey}]. Exp: ${expValue}, Got: ${record[expKey]}`); 58 | res = false; 59 | } 60 | } 61 | 62 | // Make sure there are no unexpected keys 63 | for (const gotKey of Object.keys(record)) { 64 | if (!Object.keys(expected).includes(gotKey)) { 65 | testFailureLog(`Got unexpected key: ${gotKey}`); 66 | res = false; 67 | } 68 | } 69 | 70 | return res; 71 | } 72 | 73 | // Wrapper to run validateLogRecord against multiple lines 74 | export function validateLogRecords(records: any[], expecteds: any[]): boolean { 75 | 76 | let res = true; 77 | 78 | // Make sure the lengths are the same 79 | if (records.length !== expecteds.length) { 80 | testFailureLog(`Length mismatch. Exp: ${expecteds.length}, Got: ${records.length}`); 81 | res = false; 82 | } 83 | 84 | // Iterate the lines and compare in parallel 85 | const iterSize = Math.min(records.length, expecteds.length); 86 | for (let i = 0; i < iterSize; i++) { 87 | if (!validateLogRecord(records[i], expecteds[i])) { 88 | testFailureLog(`Line mismatch on ${i}`); 89 | res = false; 90 | } 91 | } 92 | return res; 93 | } 94 | 95 | // Get a list of records from a writable stream 96 | export function getLogRecords(logStream: Writable): string[] { 97 | return logStream.toString().split('\n').filter( 98 | (line) => line !== '').map( 99 | (line) => JSON.parse(line)); 100 | } 101 | 102 | // Helper to create a stub record for validation where all default fields are 103 | // just set to IS_PRESENT 104 | export function stubValidationRecord(): any { 105 | return deepCopy({ 106 | channel: IS_PRESENT, 107 | level: IS_PRESENT, 108 | level_str: IS_PRESENT, 109 | timestamp: IS_PRESENT, 110 | message: IS_PRESENT, 111 | num_indent: IS_PRESENT, 112 | }); 113 | } 114 | 115 | // Helper to directly serialize a record to json with no reformatting 116 | export function DirectJsonFormatter(record: any): string { 117 | return JSON.stringify(record); 118 | } 119 | 120 | //// Pretty Print Regexes //// 121 | // 122 | // These regexes are used to parse out the various parts of a pretty-printed 123 | // line. For clarity, they're broken up here into subparts. 124 | //// 125 | // e.g. "2019-11-25T22:48:12.993Z" 126 | const ppTimestampRegex = /([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}Z)/; 127 | 128 | // e.g. "[CHANN:INFO] " 129 | const ppHeaderRegex = /\[([^:]*):([^\]:]*)\]( ?<[^\s]*>)? ([\s]*)/; 130 | 131 | // e.g. "2019-11-25T22:48:12.993Z [CHANN:INFO] " 132 | const ppFullHeaderRegex = new RegExp(`^${ppTimestampRegex.source} ${ppHeaderRegex.source}`); 133 | 134 | // e.g. "This is a test" 135 | const ppMessageRegex = /([^\s].*)\n?/; 136 | 137 | // e.g. "* key: val" 138 | const ppMetadataRegex = /\* (.+): (.+)\n?/; 139 | 140 | // e.g. "2019-11-25T22:48:12.993Z [CHANN:INFO] This is a test" 141 | const ppMessageLineRegex = new RegExp(`${ppFullHeaderRegex.source}${ppMessageRegex.source}$`); 142 | 143 | // e.g. "2019-11-25T22:48:12.993Z [CHANN:INFO] * key: val" 144 | const ppMetadataLineRegex = new RegExp(`${ppFullHeaderRegex.source}${ppMetadataRegex.source}$`); 145 | 146 | // Inverted mapping from level shortname to numeric value 147 | const levelFromPrettyName: {[prettyName: string]: number} = {}; 148 | Object.keys(prettyLevelNames).forEach((levelName: string) => { 149 | levelFromPrettyName[prettyLevelNames[levelName]] = levelFromName[levelName]; 150 | }); 151 | 152 | // Parse a pretty-print line into a record. The special field `is_metadata` will 153 | // be populated to indicate whether it is a metadata line or not 154 | export function parsePPLine(line: string): any { 155 | 156 | // Try to match first as metadata, then as a normal line 157 | let isMetadata: boolean = true; 158 | let match = line.match(ppMetadataLineRegex); 159 | if (!match) { 160 | isMetadata = false; 161 | match = line.match(ppMessageLineRegex); 162 | } 163 | if (!match) { 164 | const msg = `Un-parsable pretty-print line: [${line}]`; 165 | testFailureLog(msg); 166 | throw new Error(msg) 167 | } 168 | 169 | // Add the parts of the header that are always there 170 | const record: any = { 171 | is_metadata: isMetadata, 172 | timestamp: match[1], 173 | channel: match[2], 174 | level: levelFromPrettyName[match[3]], 175 | level_str: nameFromLevel[levelFromPrettyName[match[3]]], 176 | }; 177 | 178 | // log code 179 | if (match[4]) { 180 | record.log_code = match[4].trimLeft(); 181 | } 182 | 183 | // indent 184 | record.num_indent = match[5].length / prettyIndentation.length; 185 | 186 | // message or metadata 187 | if (isMetadata) { 188 | const key = match[6]; 189 | const val = JSON.parse(match[7]); 190 | record[key] = val; 191 | } else { 192 | record.message = match[6]; 193 | } 194 | 195 | return record; 196 | } 197 | 198 | // Helper to make a record for testing 199 | export function makeTestRecord(overrides?: any): any { 200 | return Object.assign({ 201 | timestamp: '2019-11-25T22:48:12.993Z', 202 | channel: 'TEST', 203 | level: alog.INFO, 204 | level_str: nameFromLevel[alog.INFO], 205 | num_indent: 0, 206 | message: 'asdf', 207 | }, overrides); 208 | } 209 | -------------------------------------------------------------------------------- /src/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "declarations": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "baseUrl": ".", 11 | "sourceMap": true, 12 | "lib": [ 13 | "dom", 14 | "es2017" 15 | ] 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*_test.ts", 23 | "dist" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/ts/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-no-unused-expression-chai" 5 | ], 6 | "rules": { 7 | "max-line-length": { 8 | "options": [120] 9 | }, 10 | "new-parens": true, 11 | "no-arg": true, 12 | "no-bitwise": true, 13 | "no-conditional-assignment": true, 14 | "no-consecutive-blank-lines": false, 15 | "no-console": { 16 | "severity": "warning", 17 | "options": [ 18 | "log" 19 | ] 20 | }, 21 | "interface-name": false, 22 | "quotemark": false, 23 | "trailing-comma": false, 24 | "semicolon": false, 25 | "object-literal-sort-keys": false, 26 | "no-empty-interface": false, 27 | "no-var-requires": false, 28 | "no-namespace": false 29 | }, 30 | "jsRules": { 31 | "max-line-length": { 32 | "options": [120] 33 | }, 34 | "quotemark": false, 35 | "object-literal-sort-keys": false, 36 | "no-string-literal": false 37 | } 38 | } 39 | --------------------------------------------------------------------------------