Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/structlog #2907

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1133701
added handlers dir
carolinecgilbert Mar 24, 2024
4e18784
Able to import structlog handler
MigCast9 Mar 27, 2024
21e5c62
Minor changes: removed useless stuff
MigCast9 Mar 27, 2024
2e22c2e
Parth Doshi - Added Test Cases
doshi36 Mar 29, 2024
12a4035
all structlog tests pass except test_call_method_processes_log_correc…
carolinecgilbert Mar 29, 2024
097be29
Caroline Gilbert: fixed tests - all pass
carolinecgilbert Mar 31, 2024
8bf5af4
Caroline Gilbert: fixed test name
carolinecgilbert Mar 31, 2024
8515ee0
Merge pull request #1 from carolinecgilbert/structlog-testing
doshi36 Apr 10, 2024
39cde55
loguru handler
MigCast9 Apr 11, 2024
3a64898
Buggy Test Cases
doshi36 Apr 16, 2024
45aa0a3
Caroline Gilbert: new structlog tests pass
carolinecgilbert Apr 17, 2024
96548d6
Merge pull request #2 from carolinecgilbert/structlog-testing
MigCast9 Apr 17, 2024
804b579
All but one tests passing
MigCast9 Apr 27, 2024
2a84a08
Final test not passing for loguru
MigCast9 Apr 28, 2024
952c003
CG: loguru tests pass 100%
carolinecgilbert Apr 28, 2024
21e411e
Merge branch 'main' into loguru_handler_WORKING
carolinecgilbert Apr 28, 2024
3d3ae4f
Merge pull request #3 from carolinecgilbert/loguru_handler_WORKING
MigCast9 Apr 28, 2024
170ba02
removed old import file
carolinecgilbert Apr 28, 2024
a097455
CG: added structlog example
carolinecgilbert May 1, 2024
e083565
CG: loguru finalized with documentation
carolinecgilbert May 3, 2024
4561989
removed print statement
carolinecgilbert May 3, 2024
37ef8f7
updated app
carolinecgilbert May 3, 2024
27bf6d9
updated loguru app
carolinecgilbert May 3, 2024
5a2e2ee
Merge pull request #4 from carolinecgilbert/handler_examples
MigCast9 May 3, 2024
32881ce
Update README.md
MigCast9 May 3, 2024
1c92c51
Update README.md
MigCast9 May 3, 2024
ab5c238
Merge branch 'main' into carolinecgilbert/main
znd4 Oct 15, 2024
b904594
remove loguru
znd4 Oct 15, 2024
3deb254
bump opentelemetry-exporter-otlp version
znd4 Oct 15, 2024
59d0d33
re-add no-op test
znd4 Oct 15, 2024
47b5c4e
pre-commit
znd4 Oct 15, 2024
addd491
processed_event -> _
znd4 Oct 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions examples/handlers/opentelemetry_structlog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# OpenTelemetry Python `structlog` Handler Example with Docker
This is a demo for the custom structlog handler implemented for OpenTelemetry. Overall, this example runs a basic Flask application with Docker to demonstrate an example application that uses OpenTelemetry logging with Python's logging library structlog. This example is scalable to other software systems that require the use of the structlog library for logging.

Note: This example is adapted from OpenTelemetry's [Getting Started Tutorial for Python](https://opentelemetry.io/docs/languages/python/getting-started/) guide and OpenTelemetry's [example for logs](https://github.com/open-telemetry/opentelemetry-python/blob/main/docs/examples/logs/README.rst) code.

## Prerequisites
Python 3

## Installation
Prior to building the example application, set up the directory and virtual environment:
```
mkdir otel-structlog-example
cd otel-structlog-example
python3 -m venv venv
source ./venv/bin/activate
```

After activating the virtual environment `venv`, install flask and structlog.
```
pip install flask
pip install structlog
pip install opentelemetry-exporter-otlp
```

### Create and Launch HTTP Server
Now that the environment is set up, create an `app.py` flask application. This is a basic example that uses the structlog Python logging library for OpenTelemetry logging instead of the standard Python logging library.

Notice the importance of the following imports for using the structlog handler: `import structlog` and `from handlers.opentelemetry_structlog.src.exporter import StructlogHandler`.

```
from random import randint
from flask import Flask, request
import structlog
import sys
sys.path.insert(0, '../../..')
from handlers.opentelemetry_structlog.src.exporter import StructlogHandler
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource

logger_provider = LoggerProvider(
resource=Resource.create(
{
"service.name": "shoppingcart",
"service.instance.id": "instance-12",
}
),
)
set_logger_provider(logger_provider)

# Replace the standard logging configuration with structlog
structlog_handler = StructlogHandler(service_name="flask-structlog-demo", server_hostname="instance-1", exporter=OTLPLogExporter(insecure=True))
structlog_handler._logger_provider = logger_provider
structlog_logger = structlog.wrap_logger(structlog.get_logger(), processors=[structlog_handler]) # Add StructlogHandler to the logger

app = Flask(__name__)

@app.route("/rolldice")
def roll_dice():
player = request.args.get('player', default=None, type=str)
result = str(roll())
if player:
structlog_logger.warning("Player %s is rolling the dice: %s", player, result, level="warning")
else:
structlog_logger.warning("Anonymous player is rolling the dice: %s", result, level="warning")
return result


def roll():
return randint(1, 6)
```

Run the application on port 8080 with the following flask command and open [http://localhost:8080/rolldice](http://localhost:8080/rolldice) in your web browser to ensure it is working.

```
flask run -p 8080
```

However, do not be alarmed if you receive these errors since Docker is not yet set up to export the logs:
```
Transient error StatusCode.UNAVAILABLE encountered while exporting logs to localhost:4317, retrying in 1s.
Transient error StatusCode.UNAVAILABLE encountered while exporting logs to localhost:4317, retrying in 2s.
Transient error StatusCode.UNAVAILABLE encountered while exporting logs to localhost:4317, retrying in 4s.
...
```

## Run with Docker

To serve the application on Docker, first create the `otel-collector-config.yaml` file locally in the application's repository.
```
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:

processors:
batch:

exporters:
logging:
verbosity: detailed

service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [logging]
```

Next, start the Docker container:
```
docker run \
-p 4317:4317 \
-v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \
otel/opentelemetry-collector-contrib:latest
```

And lastly, run the basic application with flask:
```
flask run -p 8080
```

Here is some example output:
```
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:8080
Press CTRL+C to quit
2024-04-28 23:15:22 [warning ] Anonymous player is rolling the dice: 1
127.0.0.1 - - [28/Apr/2024 23:15:22] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:27 [warning ] Anonymous player is rolling the dice: 6
127.0.0.1 - - [28/Apr/2024 23:15:27] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:28 [warning ] Anonymous player is rolling the dice: 3
127.0.0.1 - - [28/Apr/2024 23:15:28] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:29 [warning ] Anonymous player is rolling the dice: 4
127.0.0.1 - - [28/Apr/2024 23:15:29] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:29 [warning ] Anonymous player is rolling the dice: 1
127.0.0.1 - - [28/Apr/2024 23:15:29] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:30 [warning ] Anonymous player is rolling the dice: 2
127.0.0.1 - - [28/Apr/2024 23:15:30] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:15:31 [warning ] Anonymous player is rolling the dice: 3
127.0.0.1 - - [28/Apr/2024 23:15:31] "GET /rolldice HTTP/1.1" 200 -
2024-04-28 23:16:14 [warning ] Anonymous player is rolling the dice: 4
127.0.0.1 - - [28/Apr/2024 23:16:14] "GET /rolldice HTTP/1.1" 200 -
```


## Contributors
Caroline Gilbert: [carolincgilbert](https://github.com/carolinecgilbert)
60 changes: 60 additions & 0 deletions examples/handlers/opentelemetry_structlog/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys
from random import randint

import structlog
from flask import Flask, request

sys.path.insert(0, "../../..")
from handlers.opentelemetry_structlog.src.exporter import StructlogHandler
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.resources import Resource

logger_provider = LoggerProvider(
resource=Resource.create(
{
"service.name": "shoppingcart",
"service.instance.id": "instance-12",
}
),
)
set_logger_provider(logger_provider)

# Replace the standard logging configuration with structlog
structlog_handler = StructlogHandler(
service_name="flask-structlog-demo",
server_hostname="instance-1",
exporter=OTLPLogExporter(insecure=True),
)
structlog_handler._logger_provider = logger_provider
structlog_logger = structlog.wrap_logger(
structlog.get_logger(), processors=[structlog_handler]
) # Add StructlogHandler to the logger


app = Flask(__name__)


@app.route("/rolldice")
def roll_dice():
player = request.args.get("player", default=None, type=str)
result = str(roll())
if player:
structlog_logger.warning(
"Player %s is rolling the dice: %s",
player,
result,
level="warning",
)
else:
structlog_logger.warning(
"Anonymous player is rolling the dice: %s", result, level="warning"
)
return result


def roll():
return randint(1, 6)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:

processors:
batch:

exporters:
logging:
verbosity: detailed

service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [logging]
Empty file added handlers/__init__.py
Empty file.
1 change: 1 addition & 0 deletions handlers/opentelemetry_structlog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Structlog handler for OpenTelemetry
Empty file.
45 changes: 45 additions & 0 deletions handlers/opentelemetry_structlog/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[build-system]
requires = [
"hatchling",
]
build-backend = "hatchling.build"

[project]
name = "opentelemetry-structlog"
dynamic = [
"version",
]
description = "Structlog handler for emitting logs to OpenTelemetry"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.7"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"opentelemetry-sdk ~= 1.22",
"structlog ~= 24.1",
]

[tool.hatch.version]
path = "src/opentelemetry-structlog/version.py"

[tool.hatch.build.targets.sdist]
include = [
"/src",
]

[tool.hatch.build.targets.wheel]
packages = [
"src/opentelemetry-structlog",
]
60 changes: 60 additions & 0 deletions handlers/opentelemetry_structlog/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Structlog Handler for OpenTelemetry
This project provides a Structlog handler for OpenTelemetry applications. The handler converts Structlog logs into the OpenTelemetry Logs Protocol (OTLP) format for export to a collector.

## Usage

To use the Structlog handler in your OpenTelemetry application, follow these steps:

1. Import the necessary modules:

```python
import structlog
from opentelemetry.sdk._logs._internal.export import LogExporter
from opentelemetry.sdk.resources import Resource
from handlers.opentelemetry_structlog.src.exporter import StructlogHandler
```

2. Initialize the StructlogHandler with your service name, server hostname, and LogExporter instance:

```python
service_name = "my_service"
server_hostname = "my_server"
exporter = LogExporter() # Initialize your LogExporter instance
handler = StructlogHandler(service_name, server_hostname, exporter)
```

3. Add the handler to your Structlog logger:

```python
structlog.configure(
processors=[structlog.processors.JSONRenderer()],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
context_class=dict,
**handler.wrap_for_structlog(),
)
```

4. Use the logger as usual with Structlog:

```python
logger = structlog.get_logger()
logger.info("This is a test log message.")
```
## OpenTelemetry Application Example with Handler
See the structlog handler demo in the examples directory of this repository for a step-by-step guide on using the handler in an OpenTelemetry application.

## Customization

The StructlogHandler supports customization through its constructor parameters:

- `service_name`: The name of your service.
- `server_hostname`: The hostname of the server where the logs originate.
- `exporter`: An instance of your LogExporter for exporting logs to a collector.

## Notes

- This handler automatically converts the `timestamp` key in the `event_dict` to ISO 8601 format for better compatibility.
- It performs operations similar to `structlog.processors.ExceptionRenderer`, so avoid using `ExceptionRenderer` in the same pipeline.
```
Empty file.
Loading