Skip to content

Commit

Permalink
OTLP telemetry exporter (#213)
Browse files Browse the repository at this point in the history
This PR updates the telemetry subsystem to support 1) formatting log
entries to OTLP and 2) export logs/traces to configurable OTLP backends.

For instance, with a service that accepts both traces and logs (like the
OTel collector), the DBOS SDK can be configure to export both signals to
this service:
```yaml
telemetry:
    OTLPExporters:
        - tracesEndpoint: 'http://localhost:4318/v1/traces'
        - logsEndpoint: 'http://localhost:4318/v1/logs'
```
Note this setup works out of the box with Jaeger and the OTel collector
(see screenshots bellow.)
Also note `opentelemetry-js` logs API is experimental, so I fixed the
version we use to `0.41.2`

**Details**
- Use a wrapper around the Winston logger everywhere.
- The wrapper adds a new OTLP transport to the wrapped winston logger if
it detects configured exporters
- This transport simply formats an OTel `LogRecord` and push it to our
collector
- Massively simplify our typing by only using opentelemetry `LogRecord`
and `Span`

**Others**
- Metadata are injected to all Winston transports and the transport
logic decides whether to include them or not

**Thoughts**
- There exists a neater way to integrate the active span context to each
OTLP log records, which we can consider in the future. Talk to me for
details or see PR comments about having a context manager
- The Winston folks are actively working on an OTLP transport/exporter
themselves, see
[here](open-telemetry/opentelemetry-js-contrib#1558)
and
[there](open-telemetry/opentelemetry-js-contrib#1837).
In the future we might want to use it.

**Tests**

☑️ Unit tests: ideally we would be able to intercept outgoing HTTP
requests and expect the payload to conform our expectations. This seems
to be quite some work so pushing that for later.

✅ Tested with DBOS cloud integration tests.

✅ Logs w/o metadata:
![Screenshot 2023-12-11 at 11 57
42](https://github.com/dbos-inc/dbos-ts/assets/3437048/093efe7a-6ff2-4ec0-bf6b-f4d9e30f7ed9)


✅ Logs w/ metadata:
![Screenshot 2023-12-11 at 12 01
02](https://github.com/dbos-inc/dbos-ts/assets/3437048/8a8ed3ff-37a7-4632-aee4-d140bfb0a549)


✅ Logs & traces exported to a local OTel collector
![Screenshot 2023-12-10 at 14 23
25](https://github.com/dbos-inc/dbos-ts/assets/3437048/292acc9e-a7f6-47e5-bb58-f7216d8afcfd)

✅ Traces exported to a local Jaeger:
<img width="1784" alt="jaeger"
src="https://github.com/dbos-inc/dbos-ts/assets/3437048/89b6e8dc-a2da-43c6-9f4f-821ed31c2159">
  • Loading branch information
maxdml committed Dec 11, 2023
1 parent 6a3a23a commit 499dfed
Show file tree
Hide file tree
Showing 26 changed files with 378 additions and 520 deletions.
60 changes: 39 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@koa/cors": "^4.0.0",
"@koa/router": "^12.0.0",
"@opentelemetry/api": "^1.4.1",
"@opentelemetry/exporter-logs-otlp-http": "0.41.2",
"@opentelemetry/exporter-trace-otlp-http": "^0.41.2",
"@opentelemetry/sdk-trace-base": "^1.15.2",
"@types/koa": "^2.13.8",
Expand Down
8 changes: 4 additions & 4 deletions src/cloud-cli/applications/configure.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import axios from "axios";
import fs from "fs";
import YAML from "yaml";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";
import { ConfigFile, loadConfigFile, dbosConfigFilePath } from "../../dbos-runtime/config";
import { execSync } from "child_process";

export async function configureApp(host: string, port: string, dbName: string) {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

// call cloud and get hostname and port
const res = await axios.get(`http://${host}:${port}/${userCredentials.userName}/databases/userdb/info/${dbName}`,
const res = await axios.get(`http://${host}:${port}/${userCredentials.userName}/databases/userdb/info/${dbName}`,
{
headers: {
"Content-Type": "application/json",
Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/applications/delete-app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import axios from "axios";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";

export async function deleteApp(appName: string, host: string, port: string): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/applications/deploy-app-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import axios from "axios";
import YAML from "yaml";
import { execSync } from "child_process";
import fs from "fs";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";
import { createDirectory, readFileSync } from "../../utils";
import { ConfigFile, loadConfigFile, dbosConfigFilePath } from "../../dbos-runtime/config";

const deployDirectoryName = "dbos_deploy";

export async function deployAppCode(appName: string, host: string, port: string): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/applications/get-app-logs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import axios from "axios";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";

export async function getAppLogs(appName: string, host: string, port: string): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/applications/list-apps.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import axios from "axios";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";
import { Application } from "./types";

export async function listApps(host: string, port: string): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/applications/register-app.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import axios from "axios";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";

export async function registerApp(appName: string, host: string, port: string, machines: number): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down
7 changes: 4 additions & 3 deletions src/cloud-cli/applications/update-app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import axios from "axios";
import { createGlobalLogger } from "../../telemetry/logs";
import { GlobalLogger } from "../../telemetry/logs";
import { getCloudCredentials } from "../utils";
import { Application } from "./types";

export async function updateApp(appName: string, host: string, port: string, machines: number): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down Expand Up @@ -32,7 +32,8 @@ export async function updateApp(appName: string, host: string, port: string, mac
logger.error(`failed to update application ${appName}: ${e.response?.data}`);
return 1;
} else {
logger.error(`failed to update application ${appName}: ${(e as Error).message}`);
(e as Error).message = `failed to update application ${appName}: ${(e as Error).message}`;
logger.error(e);
return 1;
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/cloud-cli/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createGlobalLogger } from "../telemetry/logs";
import { GlobalLogger } from "../telemetry/logs";
import axios from "axios";
import { sleep } from "../utils";
import jwt, { JwtPayload } from 'jsonwebtoken';
Expand Down Expand Up @@ -60,7 +60,7 @@ async function verifyToken(token: string): Promise<JwtPayload> {
}

export async function login(username: string): Promise<number> {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
logger.info(`Logging in!`);

const deviceCodeRequest = {
Expand All @@ -74,7 +74,8 @@ export async function login(username: string): Promise<number> {
const response = await axios.request(deviceCodeRequest);
deviceCodeResponse = response.data as DeviceCodeResponse;
} catch (e) {
logger.error(`failed to log in`, e);
(e as Error).message = `failed to log in: ${(e as Error).message}`;
logger.error(e);
}
if (!deviceCodeResponse) {
return 1;
Expand Down Expand Up @@ -112,7 +113,7 @@ export async function login(username: string): Promise<number> {
const credentials: DBOSCloudCredentials = {
token: tokenResponse.access_token,
userName: username,
}
};
execSync(`mkdir -p ${dbosEnvPath}`);
fs.writeFileSync(`${dbosEnvPath}/credentials`, JSON.stringify(credentials), "utf-8");
logger.info(`Successfully logged in as user: ${credentials.userName}`);
Expand Down
4 changes: 2 additions & 2 deletions src/cloud-cli/register.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import axios from "axios";
import { createGlobalLogger } from "../telemetry/logs";
import { GlobalLogger } from "../telemetry/logs";
import { getCloudCredentials } from "./utils";

export async function registerUser(username: string, host: string, port: string): Promise<number> {
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;
const userName = userCredentials.userName;
const logger = createGlobalLogger();
const logger = new GlobalLogger();
try {
// First, register the user.
const register = await axios.put(
Expand Down
15 changes: 7 additions & 8 deletions src/cloud-cli/userdb.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import axios from "axios";
import { createGlobalLogger } from "../telemetry/logs";
import { GlobalLogger } from "../telemetry/logs";
import { getCloudCredentials } from "./utils";
import { sleep } from "../utils";
import { ConfigFile, loadConfigFile, dbosConfigFilePath } from "../dbos-runtime/config";
import { execSync } from "child_process";

export async function createUserDb(host: string, port: string, dbName: string, adminName: string, adminPassword: string, sync: boolean) {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down Expand Up @@ -66,7 +66,7 @@ export async function createUserDb(host: string, port: string, dbName: string, a
}

export async function deleteUserDb(host: string, port: string, dbName: string, sync: boolean) {
const logger = createGlobalLogger();
const logger = new GlobalLogger();
const userCredentials = getCloudCredentials();
const bearerToken = "Bearer " + userCredentials.token;

Expand Down Expand Up @@ -122,7 +122,7 @@ export async function deleteUserDb(host: string, port: string, dbName: string, s
}

export async function getUserDb(host: string, port: string, dbName: string) {
const logger = createGlobalLogger();
const logger = new GlobalLogger();

try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand All @@ -138,7 +138,7 @@ export async function getUserDb(host: string, port: string, dbName: string) {
}

export function migrate(): number {
const logger = createGlobalLogger();
const logger = new GlobalLogger();

// read the yaml file
const configFile: ConfigFile | undefined = loadConfigFile(dbosConfigFilePath);
Expand Down Expand Up @@ -199,8 +199,7 @@ export function migrate(): number {
logger.error(e.message);
}
} else {
// If 'e' is not an Error object, log it as a generic error
logger.error('An unknown error occurred:', e);
logger.error(e);
}
return 1;
}
Expand All @@ -209,7 +208,7 @@ export function migrate(): number {
}

export function rollbackmigration(): number {
const logger = createGlobalLogger();
const logger = new GlobalLogger();

// read the yaml file
const configFile: ConfigFile | undefined = loadConfigFile(dbosConfigFilePath);
Expand Down
2 changes: 1 addition & 1 deletion src/communicator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Span } from "@opentelemetry/sdk-trace-base";
import { WinstonLogger as Logger } from "./telemetry/logs";
import { GlobalLogger as Logger } from "./telemetry/logs";
import { WorkflowContextImpl } from "./workflow";
import { DBOSContext, DBOSContextImpl } from "./context";
import { WorkflowContextDebug } from "./debugger/debug_workflow";
Expand Down
4 changes: 2 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Span } from "@opentelemetry/sdk-trace-base";
import { WinstonLogger as Logger, Logger as DBOSLogger } from "./telemetry/logs";
import { GlobalLogger as Logger, Logger as DBOSLogger } from "./telemetry/logs";
import { has, get } from "lodash";
import { IncomingHttpHeaders } from "http";
import { ParsedUrlQuery } from "querystring";
Expand Down Expand Up @@ -44,14 +44,14 @@ export class DBOSContextImpl implements DBOSContext {
readonly logger: DBOSLogger; // Wrapper around the global logger for this context.

constructor(readonly operationName: string, readonly span: Span, logger: Logger, parentCtx?: DBOSContextImpl) {
this.logger = new DBOSLogger(logger, this);
if (parentCtx) {
this.request = parentCtx.request;
this.authenticatedUser = parentCtx.authenticatedUser;
this.authenticatedRoles = parentCtx.authenticatedRoles;
this.assumedRole = parentCtx.assumedRole;
this.workflowUUID = parentCtx.workflowUUID;
}
this.logger = new DBOSLogger(logger, this);
}

/*** Application configuration ***/
Expand Down

0 comments on commit 499dfed

Please sign in to comment.