loki-mode 7.48.0 → 7.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +85 -2
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +124 -0
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +258 -12
- package/dashboard/audit.py +202 -21
- package/dashboard/server.py +64 -10
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
package/dashboard/server.py
CHANGED
|
@@ -55,6 +55,11 @@ from . import app_secrets as secrets_mod
|
|
|
55
55
|
from . import telemetry as _telemetry
|
|
56
56
|
from .control import atomic_write_json, find_skill_dir, is_process_running
|
|
57
57
|
from .activity_logger import get_activity_logger
|
|
58
|
+
from .api_v2 import (
|
|
59
|
+
TenantContext,
|
|
60
|
+
_enforce_project_tenant,
|
|
61
|
+
resolve_tenant_context,
|
|
62
|
+
)
|
|
58
63
|
|
|
59
64
|
try:
|
|
60
65
|
from . import __version__ as _version
|
|
@@ -267,6 +272,7 @@ class ProjectResponse(BaseModel):
|
|
|
267
272
|
description: Optional[str]
|
|
268
273
|
prd_path: Optional[str]
|
|
269
274
|
status: str
|
|
275
|
+
tenant_id: int
|
|
270
276
|
created_at: datetime
|
|
271
277
|
updated_at: datetime
|
|
272
278
|
task_count: int = 0
|
|
@@ -1255,14 +1261,25 @@ async def list_projects(
|
|
|
1255
1261
|
limit: int = Query(default=50, ge=1, le=500),
|
|
1256
1262
|
offset: int = Query(default=0, ge=0),
|
|
1257
1263
|
db: AsyncSession = Depends(get_db),
|
|
1264
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1258
1265
|
) -> list[ProjectResponse]:
|
|
1259
|
-
"""List projects with pagination. Does not eager-load tasks for efficiency.
|
|
1266
|
+
"""List projects with pagination. Does not eager-load tasks for efficiency.
|
|
1267
|
+
|
|
1268
|
+
Tenant isolation (P3-7): a non-admin authenticated caller only sees
|
|
1269
|
+
projects belonging to their declared tenant (X-Loki-Tenant-ID). A global
|
|
1270
|
+
admin and single-user local mode (auth disabled) see all projects.
|
|
1271
|
+
"""
|
|
1260
1272
|
try:
|
|
1261
1273
|
from sqlalchemy import func as sa_func
|
|
1262
1274
|
|
|
1263
1275
|
query = select(Project)
|
|
1264
1276
|
if status:
|
|
1265
1277
|
query = query.where(Project.status == status)
|
|
1278
|
+
if not tenant_ctx.is_global_admin and tenant_ctx.auth_enabled:
|
|
1279
|
+
# Pin to the caller's tenant. A None tenant_id yields no matches,
|
|
1280
|
+
# which is the correct fail-closed behaviour for a scoped caller
|
|
1281
|
+
# that did not declare a tenant.
|
|
1282
|
+
query = query.where(Project.tenant_id == tenant_ctx.tenant_id)
|
|
1266
1283
|
query = query.order_by(Project.created_at.desc()).offset(offset).limit(limit)
|
|
1267
1284
|
|
|
1268
1285
|
result = await db.execute(query)
|
|
@@ -1295,6 +1312,7 @@ async def list_projects(
|
|
|
1295
1312
|
description=project.description,
|
|
1296
1313
|
prd_path=project.prd_path,
|
|
1297
1314
|
status=project.status,
|
|
1315
|
+
tenant_id=project.tenant_id,
|
|
1298
1316
|
created_at=project.created_at,
|
|
1299
1317
|
updated_at=project.updated_at,
|
|
1300
1318
|
task_count=total,
|
|
@@ -1311,8 +1329,14 @@ async def list_projects(
|
|
|
1311
1329
|
async def create_project(
|
|
1312
1330
|
project: ProjectCreate,
|
|
1313
1331
|
db: AsyncSession = Depends(get_db),
|
|
1332
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1314
1333
|
) -> ProjectResponse:
|
|
1315
|
-
"""Create a new project.
|
|
1334
|
+
"""Create a new project.
|
|
1335
|
+
|
|
1336
|
+
Tenant isolation (P3-7): a non-admin caller may only create projects in
|
|
1337
|
+
their own declared tenant; targeting another tenant returns 403.
|
|
1338
|
+
"""
|
|
1339
|
+
tenant_ctx.enforce(project.tenant_id)
|
|
1316
1340
|
# Validate tenant exists
|
|
1317
1341
|
tenant_result = await db.execute(
|
|
1318
1342
|
select(Tenant).where(Tenant.id == project.tenant_id)
|
|
@@ -1342,6 +1366,7 @@ async def create_project(
|
|
|
1342
1366
|
description=db_project.description,
|
|
1343
1367
|
prd_path=db_project.prd_path,
|
|
1344
1368
|
status=db_project.status,
|
|
1369
|
+
tenant_id=db_project.tenant_id,
|
|
1345
1370
|
created_at=db_project.created_at,
|
|
1346
1371
|
updated_at=db_project.updated_at,
|
|
1347
1372
|
task_count=0,
|
|
@@ -1353,8 +1378,9 @@ async def create_project(
|
|
|
1353
1378
|
async def get_project(
|
|
1354
1379
|
project_id: int,
|
|
1355
1380
|
db: AsyncSession = Depends(get_db),
|
|
1381
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1356
1382
|
) -> ProjectResponse:
|
|
1357
|
-
"""Get a project by ID."""
|
|
1383
|
+
"""Get a project by ID, scoped to the caller's tenant boundary."""
|
|
1358
1384
|
result = await db.execute(
|
|
1359
1385
|
select(Project)
|
|
1360
1386
|
.options(selectinload(Project.tasks))
|
|
@@ -1365,6 +1391,8 @@ async def get_project(
|
|
|
1365
1391
|
if not project:
|
|
1366
1392
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
1367
1393
|
|
|
1394
|
+
tenant_ctx.enforce(project.tenant_id)
|
|
1395
|
+
|
|
1368
1396
|
task_count = len(project.tasks)
|
|
1369
1397
|
completed_count = len([t for t in project.tasks if t.status == TaskStatus.DONE])
|
|
1370
1398
|
|
|
@@ -1374,6 +1402,7 @@ async def get_project(
|
|
|
1374
1402
|
description=project.description,
|
|
1375
1403
|
prd_path=project.prd_path,
|
|
1376
1404
|
status=project.status,
|
|
1405
|
+
tenant_id=project.tenant_id,
|
|
1377
1406
|
created_at=project.created_at,
|
|
1378
1407
|
updated_at=project.updated_at,
|
|
1379
1408
|
task_count=task_count,
|
|
@@ -1386,8 +1415,9 @@ async def update_project(
|
|
|
1386
1415
|
project_id: int,
|
|
1387
1416
|
project_update: ProjectUpdate,
|
|
1388
1417
|
db: AsyncSession = Depends(get_db),
|
|
1418
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1389
1419
|
) -> ProjectResponse:
|
|
1390
|
-
"""Update a project."""
|
|
1420
|
+
"""Update a project, scoped to the caller's tenant boundary."""
|
|
1391
1421
|
result = await db.execute(
|
|
1392
1422
|
select(Project)
|
|
1393
1423
|
.options(selectinload(Project.tasks))
|
|
@@ -1398,6 +1428,8 @@ async def update_project(
|
|
|
1398
1428
|
if not project:
|
|
1399
1429
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
1400
1430
|
|
|
1431
|
+
tenant_ctx.enforce(project.tenant_id)
|
|
1432
|
+
|
|
1401
1433
|
update_data = project_update.model_dump(exclude_unset=True)
|
|
1402
1434
|
for field, value in update_data.items():
|
|
1403
1435
|
setattr(project, field, value)
|
|
@@ -1420,6 +1452,7 @@ async def update_project(
|
|
|
1420
1452
|
description=project.description,
|
|
1421
1453
|
prd_path=project.prd_path,
|
|
1422
1454
|
status=project.status,
|
|
1455
|
+
tenant_id=project.tenant_id,
|
|
1423
1456
|
created_at=project.created_at,
|
|
1424
1457
|
updated_at=project.updated_at,
|
|
1425
1458
|
task_count=task_count,
|
|
@@ -1432,8 +1465,9 @@ async def delete_project(
|
|
|
1432
1465
|
project_id: int,
|
|
1433
1466
|
request: Request,
|
|
1434
1467
|
db: AsyncSession = Depends(get_db),
|
|
1468
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1435
1469
|
) -> None:
|
|
1436
|
-
"""Delete a project."""
|
|
1470
|
+
"""Delete a project, scoped to the caller's tenant boundary."""
|
|
1437
1471
|
result = await db.execute(
|
|
1438
1472
|
select(Project).where(Project.id == project_id)
|
|
1439
1473
|
)
|
|
@@ -1442,6 +1476,8 @@ async def delete_project(
|
|
|
1442
1476
|
if not project:
|
|
1443
1477
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
1444
1478
|
|
|
1479
|
+
tenant_ctx.enforce(project.tenant_id)
|
|
1480
|
+
|
|
1445
1481
|
audit.log_event(
|
|
1446
1482
|
action="delete",
|
|
1447
1483
|
resource_type="project",
|
|
@@ -1702,8 +1738,11 @@ async def list_tasks(
|
|
|
1702
1738
|
async def create_task(
|
|
1703
1739
|
task: TaskCreate,
|
|
1704
1740
|
db: AsyncSession = Depends(get_db),
|
|
1741
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1705
1742
|
) -> TaskResponse:
|
|
1706
|
-
"""Create a new task."""
|
|
1743
|
+
"""Create a new task, scoped to the caller's tenant boundary."""
|
|
1744
|
+
# Enforce the tenant boundary on the target project (also 404s if missing).
|
|
1745
|
+
await _enforce_project_tenant(db, tenant_ctx, task.project_id)
|
|
1707
1746
|
# Verify project exists
|
|
1708
1747
|
result = await db.execute(
|
|
1709
1748
|
select(Project).where(Project.id == task.project_id)
|
|
@@ -1777,8 +1816,9 @@ async def create_task(
|
|
|
1777
1816
|
async def get_task(
|
|
1778
1817
|
task_id: int,
|
|
1779
1818
|
db: AsyncSession = Depends(get_db),
|
|
1819
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1780
1820
|
) -> TaskResponse:
|
|
1781
|
-
"""Get a task by ID."""
|
|
1821
|
+
"""Get a task by ID, scoped to the caller's tenant boundary."""
|
|
1782
1822
|
result = await db.execute(
|
|
1783
1823
|
select(Task).where(Task.id == task_id)
|
|
1784
1824
|
)
|
|
@@ -1787,6 +1827,8 @@ async def get_task(
|
|
|
1787
1827
|
if not task:
|
|
1788
1828
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1789
1829
|
|
|
1830
|
+
await _enforce_project_tenant(db, tenant_ctx, task.project_id)
|
|
1831
|
+
|
|
1790
1832
|
return _task_response_from_db(task)
|
|
1791
1833
|
|
|
1792
1834
|
|
|
@@ -1795,8 +1837,9 @@ async def update_task(
|
|
|
1795
1837
|
task_id: int,
|
|
1796
1838
|
task_update: TaskUpdate,
|
|
1797
1839
|
db: AsyncSession = Depends(get_db),
|
|
1840
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1798
1841
|
) -> TaskResponse:
|
|
1799
|
-
"""Update a task."""
|
|
1842
|
+
"""Update a task, scoped to the caller's tenant boundary."""
|
|
1800
1843
|
result = await db.execute(
|
|
1801
1844
|
select(Task).where(Task.id == task_id)
|
|
1802
1845
|
)
|
|
@@ -1805,6 +1848,8 @@ async def update_task(
|
|
|
1805
1848
|
if not task:
|
|
1806
1849
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1807
1850
|
|
|
1851
|
+
await _enforce_project_tenant(db, tenant_ctx, task.project_id)
|
|
1852
|
+
|
|
1808
1853
|
update_data = task_update.model_dump(exclude_unset=True)
|
|
1809
1854
|
|
|
1810
1855
|
# Handle status change to/from completed
|
|
@@ -1844,8 +1889,9 @@ async def delete_task(
|
|
|
1844
1889
|
task_id: int,
|
|
1845
1890
|
request: Request,
|
|
1846
1891
|
db: AsyncSession = Depends(get_db),
|
|
1892
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1847
1893
|
) -> None:
|
|
1848
|
-
"""Delete a task."""
|
|
1894
|
+
"""Delete a task, scoped to the caller's tenant boundary."""
|
|
1849
1895
|
result = await db.execute(
|
|
1850
1896
|
select(Task).where(Task.id == task_id)
|
|
1851
1897
|
)
|
|
@@ -1854,6 +1900,8 @@ async def delete_task(
|
|
|
1854
1900
|
if not task:
|
|
1855
1901
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1856
1902
|
|
|
1903
|
+
await _enforce_project_tenant(db, tenant_ctx, task.project_id)
|
|
1904
|
+
|
|
1857
1905
|
project_id = task.project_id
|
|
1858
1906
|
|
|
1859
1907
|
audit.log_event(
|
|
@@ -1888,8 +1936,12 @@ async def move_task(
|
|
|
1888
1936
|
task_id: int,
|
|
1889
1937
|
move: TaskMove,
|
|
1890
1938
|
db: AsyncSession = Depends(get_db),
|
|
1939
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
1891
1940
|
) -> TaskResponse:
|
|
1892
|
-
"""Move a task to a new status/position (for Kanban drag-and-drop).
|
|
1941
|
+
"""Move a task to a new status/position (for Kanban drag-and-drop).
|
|
1942
|
+
|
|
1943
|
+
Scoped to the caller's tenant boundary.
|
|
1944
|
+
"""
|
|
1893
1945
|
result = await db.execute(
|
|
1894
1946
|
select(Task).where(Task.id == task_id)
|
|
1895
1947
|
)
|
|
@@ -1898,6 +1950,8 @@ async def move_task(
|
|
|
1898
1950
|
if not task:
|
|
1899
1951
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
1900
1952
|
|
|
1953
|
+
await _enforce_project_tenant(db, tenant_ctx, task.project_id)
|
|
1954
|
+
|
|
1901
1955
|
old_status = task.status
|
|
1902
1956
|
|
|
1903
1957
|
# Validate status transition
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.
|
|
5
|
+
**Version:** v7.50.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
396
396
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
397
397
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
398
398
|
-v $(pwd):/workspace -w /workspace \
|
|
399
|
-
asklokesh/loki-mode:7.
|
|
399
|
+
asklokesh/loki-mode:7.50.0 start ./my-spec.md
|
|
400
400
|
```
|
|
401
401
|
|
|
402
402
|
##### docker compose + .env (no host install)
|
package/docs/siem-integration.md
CHANGED
|
@@ -72,6 +72,67 @@ loki syslog test --message "Test event from Loki Mode"
|
|
|
72
72
|
tail -f /var/log/loki-mode.log
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Event Export Module (CEF + Splunk HEC)
|
|
76
|
+
|
|
77
|
+
In addition to syslog forwarding, Loki Mode ships a programmatic exporter for
|
|
78
|
+
audit/security events at `src/observability/siem-export.js`. It provides two
|
|
79
|
+
well-specified, vendor-agnostic formats and an SSRF-safe HEC sender.
|
|
80
|
+
|
|
81
|
+
### Zero egress unless configured
|
|
82
|
+
|
|
83
|
+
The module follows the same gate as the OTEL bridge: nothing leaves the host
|
|
84
|
+
unless an endpoint env var is set. `createHECSenderFromEnv()` returns `null`
|
|
85
|
+
when `LOKI_SPLUNK_HEC_URL` is unset, so there is no code path to the network.
|
|
86
|
+
Endpoint URLs are validated to be `http:`/`https:` only (the same SSRF guard
|
|
87
|
+
the OTLP exporter uses), so a stray `file://` or `gopher://` endpoint is
|
|
88
|
+
rejected before any request is built.
|
|
89
|
+
|
|
90
|
+
### Auto-detected environment variables
|
|
91
|
+
|
|
92
|
+
| Variable | Required | Description |
|
|
93
|
+
|----------|----------|-------------|
|
|
94
|
+
| `LOKI_SPLUNK_HEC_URL` | enables HEC | Splunk HEC collector URL. Presence enables the sender. |
|
|
95
|
+
| `LOKI_SPLUNK_HEC_TOKEN` | no | HEC auth token (sent as `Authorization: Splunk <token>`). |
|
|
96
|
+
| `LOKI_SPLUNK_HEC_INDEX` | no | Target Splunk index. |
|
|
97
|
+
| `LOKI_SPLUNK_HEC_SOURCETYPE` | no | Sourcetype (default `loki:audit`). |
|
|
98
|
+
| `LOKI_CEF_VENDOR` / `LOKI_CEF_PRODUCT` | no | Override CEF vendor/product header fields. |
|
|
99
|
+
|
|
100
|
+
### CEF (Common Event Format)
|
|
101
|
+
|
|
102
|
+
`toCEF(entry)` converts a Loki audit entry into a single-line CEF record.
|
|
103
|
+
Header pipes/backslashes and extension `=`/newlines are escaped per the CEF
|
|
104
|
+
spec, so events cannot break the record framing. Failed events
|
|
105
|
+
(`success: false`) are elevated to severity 8.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
CEF:0|Autonomi|Loki Mode|7.49.0|revoke_token|revoke_token|8|rt=2026-02-15T14:30:00.000Z suser=alice src=10.0.0.4 cs1=token cs1Label=resourceType outcome=failure msg=expired credential loki.provider=claude loki.cost=4.25
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Nested `details` are flattened under the `loki.` namespace. Standard CEF keys
|
|
112
|
+
are used where they exist (`rt`, `suser`, `src`, `outcome`, `msg`, `cs1..cs3`).
|
|
113
|
+
|
|
114
|
+
### Splunk HEC JSON
|
|
115
|
+
|
|
116
|
+
`toHEC(entry)` wraps an audit entry in a Splunk HEC envelope (epoch-seconds
|
|
117
|
+
`time`, `sourcetype`, optional `index`, and the raw `event`). `HECSender.send()`
|
|
118
|
+
POSTs it fire-and-forget; network errors are logged, never thrown, so
|
|
119
|
+
observability never breaks the run.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
export LOKI_SPLUNK_HEC_URL=https://splunk.example.com:8088/services/collector
|
|
123
|
+
export LOKI_SPLUNK_HEC_TOKEN=your-hec-token
|
|
124
|
+
export LOKI_SPLUNK_HEC_INDEX=security
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### GitHub Enterprise SAML SSO (docs-only follow-up)
|
|
128
|
+
|
|
129
|
+
Ingesting GitHub Enterprise SAML/SSO sign-in events is a documented follow-up,
|
|
130
|
+
not a shipped code path. Those events live in the GitHub audit log API and
|
|
131
|
+
require an org-scoped admin token plus an outbound polling client, which is out
|
|
132
|
+
of scope for the local audit path. Recommended approach today: pull the GitHub
|
|
133
|
+
Enterprise audit log via its native streaming to your SIEM (Splunk/Datadog/
|
|
134
|
+
Azure Event Hubs) and correlate on `actor`/`user_id` with Loki Mode events.
|
|
135
|
+
|
|
75
136
|
## Splunk Integration
|
|
76
137
|
|
|
77
138
|
### Method 1: Splunk Universal Forwarder
|
|
@@ -262,6 +323,47 @@ export LOKI_SYSLOG_FORMAT=cef
|
|
|
262
323
|
# rt=2026-02-15T14:30:00Z suser=user cs1=claude cs1Label=Provider
|
|
263
324
|
```
|
|
264
325
|
|
|
326
|
+
## OTEL Vendor Templates (Datadog, Honeycomb)
|
|
327
|
+
|
|
328
|
+
Loki Mode already emits OpenTelemetry traces/metrics when `LOKI_OTEL_ENDPOINT`
|
|
329
|
+
is set (see `src/observability/otel.js`). Ready-to-use vendor templates live in
|
|
330
|
+
`src/observability/siem-export.js` (`OTEL_TEMPLATES`) and produce the exact set
|
|
331
|
+
of env vars to ship to a vendor. They are recipes, not egress: copy the output
|
|
332
|
+
into your shell.
|
|
333
|
+
|
|
334
|
+
### Datadog
|
|
335
|
+
|
|
336
|
+
Datadog ingests OTLP/HTTP via the Datadog Agent's OTLP receiver (default
|
|
337
|
+
`:4318`) or, agentless, via the OpenTelemetry Collector contrib exporter.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# Local Datadog Agent OTLP receiver (recommended)
|
|
341
|
+
export LOKI_OTEL_ENDPOINT=http://localhost:4318
|
|
342
|
+
export LOKI_SERVICE_NAME=loki-mode
|
|
343
|
+
|
|
344
|
+
# Agentless intake (set API key + site)
|
|
345
|
+
export LOKI_OTEL_ENDPOINT=http://localhost:4318
|
|
346
|
+
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
|
347
|
+
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,dd.site=datadoghq.com"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`site` examples: `datadoghq.com`, `datadoghq.eu`, `us5.datadoghq.com`.
|
|
351
|
+
|
|
352
|
+
### Honeycomb
|
|
353
|
+
|
|
354
|
+
Honeycomb ingests OTLP/HTTP directly. Auth is the `x-honeycomb-team` header.
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
export LOKI_OTEL_ENDPOINT=https://api.honeycomb.io # or https://api.eu1.honeycomb.io
|
|
358
|
+
export LOKI_SERVICE_NAME=loki-mode
|
|
359
|
+
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY,x-honeycomb-dataset=loki"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Note: `OTEL_EXPORTER_OTLP_HEADERS` is honored when the real `@opentelemetry`
|
|
363
|
+
SDK is installed (otel.js prefers it and falls back to the built-in JSON
|
|
364
|
+
exporter). For Datadog the local-agent path holds the API key, so the header is
|
|
365
|
+
optional there.
|
|
366
|
+
|
|
265
367
|
## Datadog Security Monitoring
|
|
266
368
|
|
|
267
369
|
### Log Collection
|