Skip to content

Commit a6789f8

Browse files
authored
Merge pull request #1 from latchbio/maximsmol/drop-slog
del slog
2 parents 53298e9 + b0346c1 commit a6789f8

File tree

7 files changed

+324
-91
lines changed

7 files changed

+324
-91
lines changed

.envrc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
#!/usr/bin/env bash
22

3-
eval "$(conda shell.bash hook)"
4-
conda activate latch-o11y
3+
source .venv/bin/activate

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dist/
2+
__pycache__

pyproject.toml

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
[project]
22
name = "latch-o11y"
3-
version = "0.1.5"
3+
version = "1.0.0"
44
description = "Observability for latch python backend services"
55
authors = [{ name = "maximsmol", email = "max@latch.bio" }]
66
dependencies = [
77
"latch-config>=0.1.5",
88
"opentelemetry-api>=1.15.0",
99
"opentelemetry-sdk>=1.15.0",
1010
"opentelemetry-exporter-otlp-proto-grpc>=1.15.0",
11-
"orjson>=3.8.5",
12-
"structlog>=22.3.0",
1311
]
1412
requires-python = "==3.11.*"
1513
readme = "README.md"
16-
packages = [{ include = "latch_o11y" }]
1714
license = { text = "CC0-1.0" }
1815

1916
[build-system]
20-
requires = ["pdm-backend"]
21-
build-backend = "pdm.backend"
17+
requires = ["hatchling"]
18+
build-backend = "hatchling.build"
2219

23-
[tool.pdm.dev-dependencies]
24-
dev = [
25-
"ruff>=0.4.2",
26-
"rich>=13.7.1",
27-
]
20+
[tool.uv]
21+
dev-dependencies = ["ruff>=0.9.5"]
2822

2923
[tool.ruff]
24+
target-version = "py311"
25+
26+
[tool.ruff.lint]
27+
preview = true
28+
3029
pydocstyle = { convention = "google" }
3130
extend-select = [
3231
"F",
@@ -40,7 +39,7 @@ extend-select = [
4039
"YTT",
4140
"ANN",
4241
"ASYNC",
43-
"TRIO",
42+
"ASYNC1",
4443
"S",
4544
# "BLE", # `raise x from y` does not work
4645
"FBT",
@@ -104,6 +103,7 @@ ignore = [
104103
"C901",
105104

106105
"T201",
106+
"T203",
107107

108108
"D415",
109109

@@ -132,23 +132,36 @@ ignore = [
132132
"PLW2901",
133133
"PLW0603",
134134

135+
"PLR0904",
135136
"PLR0911",
136137
"PLR0912",
137138
"PLR0913",
138139
"PLR0914",
139140
"PLR0915",
140141
"PLR0916",
141142
"PLR0917",
143+
"PLR1702",
142144
"PLR2004",
143145

144146
"TD001",
145147
"TD003",
146148
"TD006",
147149

148150
"TID252",
151+
152+
"PD901",
153+
154+
"UP040",
155+
156+
"SIM112",
157+
158+
"PLC1901",
159+
160+
"TCH002",
149161
]
150162

151163
[tool.ruff.format]
164+
preview = true
152165
skip-magic-trailing-comma = true
153166

154167
[tool.pyright]
File renamed without changes.

latch_o11y/o11y.py renamed to src/latch_o11y/o11y.py

Lines changed: 47 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,85 @@
11
import functools
22
import inspect
3+
from collections.abc import Awaitable, Callable, Sequence
34
from dataclasses import dataclass
4-
from datetime import datetime, timezone
5-
from pathlib import Path
6-
from typing import Awaitable, Callable, Concatenate, ParamSpec, TypeAlias, TypeVar
5+
from typing import Concatenate, Literal, ParamSpec, TypeAlias, TypeVar
76

8-
import orjson
9-
import structlog as slog
107
from latch_config.config import DatadogConfig, LoggingMode, read_config
118
from opentelemetry import trace
129
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
1310
from opentelemetry.sdk.resources import Attributes, LabelValue, Resource
14-
from opentelemetry.sdk.trace import TracerProvider
15-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
11+
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
12+
from opentelemetry.sdk.trace.export import (
13+
BatchSpanProcessor,
14+
ConsoleSpanExporter,
15+
SimpleSpanProcessor,
16+
SpanExporter,
17+
SpanExportResult,
18+
)
1619
from opentelemetry.trace import Tracer
17-
from opentelemetry.trace.span import INVALID_SPAN, INVALID_SPAN_CONTEXT, Span
18-
from structlog.types import EventDict, WrappedLogger
20+
from opentelemetry.trace.span import Span
1921

2022

21-
@dataclass(frozen=True)
22-
class Config:
23-
datadog: DatadogConfig
24-
logging_mode: LoggingMode = LoggingMode.console_json
23+
class NoopSpanExporter(SpanExporter):
24+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
25+
return SpanExportResult.SUCCESS
2526

27+
def shutdown(self) -> None: ...
2628

27-
config = read_config(Config)
28-
29+
def force_flush(self, timeout_millis: int = 30000) -> bool:
30+
return True
2931

30-
def add_timestamp(logger: WrappedLogger, name: str, x: EventDict) -> EventDict:
31-
# the default slog.processors.TimeStamper is not great with timezones
32-
utc = datetime.now(timezone.utc)
33-
x["timestamp"] = utc.astimezone().isoformat()
34-
return x
3532

33+
def format_console_span(x: ReadableSpan) -> str:
34+
ctx = x.context
3635

37-
def add_dd_otel(logger: WrappedLogger, name: str, x: EventDict) -> EventDict:
38-
s = trace.get_current_span()
39-
if s == INVALID_SPAN:
40-
return x
36+
parent_ctx = x.parent
37+
parent_id = " root "
38+
if parent_ctx is not None:
39+
parent_id = f"{parent_ctx.span_id:x}"
4140

42-
ctx = s.get_span_context()
43-
if ctx == INVALID_SPAN_CONTEXT:
44-
return x
41+
s_id = "?" * 16
42+
if ctx is not None:
43+
s_id = f"{ctx.span_id:x}"
44+
return f"<{parent_id}> |- <{s_id}> {x.name}\n"
4545

46-
x["trace_id"] = format(ctx.trace_id, "032x")
47-
x["span_id"] = format(ctx.span_id, "016x")
4846

49-
x["dd.trace_id"] = str(ctx.trace_id & 0xFFFF_FFFF_FFFF_FFFF)
50-
x["dd.span_id"] = str(ctx.span_id)
51-
52-
x["dd.service"] = config.datadog.service_name
53-
x["dd.version"] = config.datadog.service_version
54-
x["dd.env"] = config.datadog.deployment_environment
47+
@dataclass(frozen=True)
48+
class Config:
49+
datadog: DatadogConfig
50+
logging_mode: LoggingMode = LoggingMode.console_json
5551

56-
return x
5752

53+
config = read_config(Config)
5854

5955
app_tracer: Tracer
60-
# fixme(maximsmol): add support for .exception
61-
log: slog.stdlib.BoundLogger
6256

6357

64-
def setup():
65-
if config.logging_mode == LoggingMode.file_json:
66-
log_file = Path(__file__).parent.parent / "o11y_debug" / "log.json"
67-
else:
68-
log_file = Path("/dev/stdout")
69-
58+
def setup(*, span_exporter: Literal["otlp", "console", "noop"] = "otlp") -> None:
7059
service_data: Attributes = {
7160
"service.name": config.datadog.service_name,
7261
"service.version": config.datadog.service_version,
7362
"deployment.environment": config.datadog.deployment_environment,
7463
}
7564

7665
tracer_provider = TracerProvider(resource=Resource(service_data))
77-
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
66+
67+
if span_exporter == "otlp":
68+
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
69+
elif span_exporter == "console":
70+
tracer_provider.add_span_processor(
71+
SimpleSpanProcessor(ConsoleSpanExporter(formatter=format_console_span))
72+
)
73+
elif span_exporter == "noop":
74+
tracer_provider.add_span_processor(SimpleSpanProcessor(NoopSpanExporter()))
75+
7876
trace.set_tracer_provider(tracer_provider)
7977

8078
global app_tracer
8179
# todo(maximsmol): setup trace sampling based on datadog settings
8280
# todo(maximsmol): port over stuff from https://github.com/open-telemetry/opentelemetry-python-contrib/blob/934af7ea4f9b1e0294ced6a014d6eefdda156b2b/exporter/opentelemetry-exporter-datadog/src/opentelemetry/exporter/datadog/exporter.py
8381
app_tracer = trace.get_tracer(__name__)
8482

85-
slog.configure_once(
86-
processors=[
87-
# todo(maximsmol): allow named loggers (needs a custom logging base)
88-
# slog.stdlib.add_logger_name,
89-
slog.stdlib.add_log_level,
90-
add_timestamp,
91-
add_dd_otel,
92-
]
93-
+ (
94-
[slog.dev.ConsoleRenderer()]
95-
if config.logging_mode == LoggingMode.console
96-
else [
97-
slog.processors.ExceptionRenderer(
98-
slog.tracebacks.ExceptionDictTransformer()
99-
),
100-
slog.processors.JSONRenderer(orjson.dumps),
101-
]
102-
),
103-
logger_factory=slog.BytesLoggerFactory(file=log_file.open("wb"))
104-
if config.logging_mode != LoggingMode.console
105-
else None,
106-
wrapper_class=slog.stdlib.AsyncBoundLogger,
107-
cache_logger_on_first_use=True,
108-
)
109-
110-
global log
111-
log = slog.stdlib.get_logger()
112-
11383

11484
T = TypeVar("T")
11585
P = ParamSpec("P")
@@ -121,7 +91,7 @@ def _trace_function_with_span_async(
12191
[Callable[Concatenate[Span, P], Awaitable[T]]], Callable[P, Awaitable[T]]
12292
]:
12393
def decorator(
124-
f: Callable[Concatenate[Span, P], Awaitable[T]]
94+
f: Callable[Concatenate[Span, P], Awaitable[T]],
12595
) -> Callable[P, Awaitable[T]]:
12696
@functools.wraps(f)
12797
async def inner(*args: P.args, **kwargs: P.kwargs) -> T:
@@ -163,7 +133,7 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> T:
163133

164134

165135
def trace_app_function_with_span(
166-
f: Callable[Concatenate[Span, P], T]
136+
f: Callable[Concatenate[Span, P], T],
167137
) -> Callable[P, T]:
168138
return trace_function_with_span(app_tracer)(f)
169139

@@ -201,13 +171,13 @@ def trace_app_function(f: Callable[P, T]) -> Callable[P, T]:
201171
return trace_function(app_tracer)(f)
202172

203173

204-
AttributesDict: TypeAlias = dict[str, LabelValue | "AttributesDict"]
174+
AttributesDict: TypeAlias = "dict[str, LabelValue | AttributesDict]"
205175

206176

207177
def dict_to_attrs(x: AttributesDict, prefix: str) -> Attributes:
208178
res: Attributes = {}
209179

210-
def inner(x: LabelValue | AttributesDict, prefix: str):
180+
def inner(x: LabelValue | AttributesDict, prefix: str) -> None:
211181
if isinstance(x, list):
212182
for i, y in enumerate(x):
213183
inner(y, f"{prefix}.{i}")
File renamed without changes.

0 commit comments

Comments
 (0)