Skip to content

style guide #49

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

Merged
merged 2 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ hatch fmt --linter

If you're using an IDE like VS Code or PyCharm, consider configuring it to use these tools automatically.

For additional details on styling, please see our dedicated [Style Guide](./STYLE_GUIDE.md).


## Contributing via Pull Requests
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
Expand Down
59 changes: 59 additions & 0 deletions STYLE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Style Guide

## Overview

The Strands Agents style guide aims to establish consistent formatting, naming conventions, and structure across all code in the repository. We strive to make our code clean, readable, and maintainable.

Where possible, we will codify these style guidelines into our linting rules and pre-commit hooks to automate enforcement and reduce the manual review burden.

## Log Formatting

The format for Strands Agents logs is as follows:

```python
logger.debug("field1=<%s>, field2=<%s>, ... | human readable message", field1, field2, ...)
```

### Guidelines

1. **Context**:
- Add context as `<FIELD>=<VALUE>` pairs at the beginning of the log
- Many log services (CloudWatch, Splunk, etc.) look for these patterns to extract fields for searching
- Use `,`'s to separate pairs
- Enclose values in `<>` for readability
- This is particularly helpful in displaying empty values (`field=` vs `field=<>`)
- Use `%s` for string interpolation as recommended by Python logging
- This is an optimization to skip string interpolation when the log level is not enabled

1. **Messages**:
- Add human readable messages at the end of the log
- Use lowercase for consistency
- Avoid punctuation (periods, exclamation points, etc.) to reduce clutter
- Keep messages concise and focused on a single statement
- If multiple statements are needed, separate them with the pipe character (`|`)
- Example: `"processing request | starting validation"`

### Examples

#### Good

```python
logger.debug("user_id=<%s>, action=<%s> | user performed action", user_id, action)
logger.info("request_id=<%s>, duration_ms=<%d> | request completed", request_id, duration)
logger.warning("attempt=<%d>, max_attempts=<%d> | retry limit approaching", attempt, max_attempts)
```

#### Poor

```python
# Avoid: No structured fields, direct variable interpolation in message
logger.debug(f"User {user_id} performed action {action}")

# Avoid: Inconsistent formatting, punctuation
logger.info("Request completed in %d ms.", duration)

# Avoid: No separation between fields and message
logger.warning("Retry limit approaching! attempt=%d max_attempts=%d", attempt, max_attempts)
```

By following these log formatting guidelines, we ensure that logs are both human-readable and machine-parseable, making debugging and monitoring more efficient.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ select = [
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"G", # logging format
"I", # isort
"LOG", # logging
]

[tool.ruff.lint.per-file-ignores]
Expand Down
10 changes: 5 additions & 5 deletions src/strands/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def __init__(
headers_dict[key.strip()] = value.strip()
otlp_headers = headers_dict
except Exception as e:
logger.warning(f"error=<{e}> | failed to parse OTEL_EXPORTER_OTLP_HEADERS")
logger.warning("error=<%s> | failed to parse OTEL_EXPORTER_OTLP_HEADERS", e)

self.service_name = service_name
self.otlp_endpoint = otlp_endpoint
Expand Down Expand Up @@ -184,9 +184,9 @@ def _initialize_tracer(self) -> None:

batch_processor = BatchSpanProcessor(otlp_exporter)
self.tracer_provider.add_span_processor(batch_processor)
logger.info(f"endpoint=<{endpoint}> | OTLP exporter configured with endpoint")
logger.info("endpoint=<%s> | OTLP exporter configured with endpoint", endpoint)
except Exception as e:
logger.error(f"error=<{e}> | Failed to configure OTLP exporter", exc_info=True)
logger.exception("error=<%s> | Failed to configure OTLP exporter", e)

# Set as global tracer provider
trace.set_tracer_provider(self.tracer_provider)
Expand Down Expand Up @@ -267,15 +267,15 @@ def _end_span(
else:
span.set_status(StatusCode.OK)
except Exception as e:
logger.warning(f"error=<{e}> | error while ending span", exc_info=True)
logger.warning("error=<%s> | error while ending span", e, exc_info=True)
finally:
span.end()
# Force flush to ensure spans are exported
if self.tracer_provider:
try:
self.tracer_provider.force_flush()
except Exception as e:
logger.warning(f"error=<{e}> | failed to force flush tracer provider")
logger.warning("error=<%s> | failed to force flush tracer provider", e)

def end_span_with_error(self, span: trace.Span, error_message: str, exception: Optional[Exception] = None) -> None:
"""End a span with error status.
Expand Down