Skip to content

Commit

Permalink
Merge pull request #166 from JaniAnttonen/development
Browse files Browse the repository at this point in the history
Merge dev to main
  • Loading branch information
JaniAnttonen authored Oct 15, 2024
2 parents 5edd9d0 + 73b2b24 commit 32795fc
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 8 deletions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ LokiTransport() takes a Javascript object as an input. These are the options tha
| `timeout` | timeout for requests to grafana loki in ms | 30000 | undefined |
| `basicAuth` | basic authentication credentials to access Loki over HTTP | username:password | undefined |
| `onConnectionError`| Loki error connection handler | (err) => console.error(err) | undefined |
| `useWinstonMetaAsLabels` | Use Winston's "meta" (such as defaultMeta values) as Loki labels | true | false |
| `ignoredMeta` | When useWinstonMetaAsLabels is enabled, a list of meta values to ignore | ["error_description"] | undefined |

### Example
### Example (Running Loki Locally)
With default formatting:
```js
const { createLogger, transports } = require("winston");
Expand All @@ -56,6 +58,37 @@ logger.debug({ message: 'test', labels: { 'key': 'value' } })

TODO: Add custom formatting example

### Example (Grafana Cloud Loki)

**Important**: this snippet requires the following values, here are the instructions for how you can find them.

* `LOKI_HOST`: find this in your Grafana Cloud instance by checking Connections > Data Sources, find the right Loki connection, and copy its URL, which may look like `https://logs-prod-006.grafana.net`
* `USER_ID`: the user number in the same data source definition, it will be a multi-digit number like `372040`
* `GRAFANA_CLOUD_TOKEN`: In Grafana Cloud, search for Cloud Access Policies. Create a new Cloud Access Policy, ensuring its scopes include `logs:write`. Generate a token for this cloud access policy, and use this value here.

```js
const { createLogger, transports } = require("winston");
const LokiTransport = require("winston-loki");
const options = {
...,
transports: [
new LokiTransport({
host: 'LOKI_HOST',
labels: { app: 'my-app' },
json: true,
basicAuth: 'USER_ID:GRAFANA_CLOUD_TOKEN',
format: winston.format.json(),
replaceTimestamp: true,
onConnectionError: (err) => console.error(err),
})
]
...
};
const logger = createLogger(options);
logger.debug({ message: 'test', labels: { 'key': 'value' } })
```


## Developing
### Requirements
Running a local Loki for testing is probably required, and the easiest way to do that is to follow this guide: https://github.com/grafana/loki/tree/master/production#run-locally-using-docker. After that, Grafana Loki instance is available at `http://localhost:3100`, with a Grafana instance running at `http://localhost:3000`. Username `admin`, password `admin`. Add the Loki source with the URL `http://loki:3100`, and the explorer should work.
Expand Down
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import TransportStream from "winston-transport";
import http from 'http';
import https from 'https';

declare interface LokiTransportOptions extends TransportStream.TransportStreamOptions{
host: string;
Expand All @@ -12,6 +14,10 @@ declare interface LokiTransportOptions extends TransportStream.TransportStreamOp
replaceTimestamp?: boolean,
gracefulShutdown?: boolean,
timeout?: number,
httpAgent?: http.Agent | boolean;
httpsAgent?: https.Agent | boolean;
useWinstonMetaAsLabels?: boolean;
ignoredMeta?: Array<string>;
onConnectionError?(error: unknown): void
}

Expand Down
14 changes: 12 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ class LokiTransport extends Transport {
onConnectionError: options.onConnectionError,
replaceTimestamp: options.replaceTimestamp !== false,
gracefulShutdown: options.gracefulShutdown !== false,
timeout: options.timeout
timeout: options.timeout,
httpAgent: options.httpAgent,
httpsAgent: options.httpsAgent
})

this.useCustomFormat = options.format !== undefined
this.labels = options.labels
this.useWinstonMetaAsLabels = options.useWinstonMetaAsLabels
this.ignoredMeta = options.ignoredMeta || []
}

/**
Expand All @@ -58,7 +62,13 @@ class LokiTransport extends Transport {
// build custom labels if provided
let lokiLabels = { level: level }

if (this.labels) {
if (this.useWinstonMetaAsLabels) {
// deleting the keys (labels) that we want to ignore from Winston's meta
for (const [key, _] of Object.entries(rest)) {
if (this.ignoredMeta.includes(key)) delete rest[key]
}
lokiLabels = Object.assign(lokiLabels, rest)
} else if (this.labels) {
lokiLabels = Object.assign(lokiLabels, this.labels)
} else {
lokiLabels.job = label
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "winston-loki",
"version": "6.1.0",
"version": "6.1.2",
"description": "A Winston transport for Grafana Loki",
"keywords": [
"winston",
Expand Down
7 changes: 5 additions & 2 deletions src/batcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ class Batcher {
const URL = this.loadUrl()
this.url = new URL(this.options.host + '/loki/api/v1/push')

const btoa = require('btoa')
// Parse basic auth parameters if given
if (options.basicAuth) {
const btoa = require('btoa')
const basicAuth = 'Basic ' + btoa(options.basicAuth)
this.options.headers = Object.assign(this.options.headers, { Authorization: basicAuth })
} else if(this.url.username && this.url.password) {
const basicAuth = 'Basic ' + btoa(this.url.username + ':' + this.url.password)
this.options.headers = Object.assign(this.options.headers, { Authorization: basicAuth })
}

// Define the batching intervals
Expand Down Expand Up @@ -251,7 +254,7 @@ class Batcher {
}

// Send the data to Grafana Loki
req.post(this.url, this.contentType, this.options.headers, reqBody, this.options.timeout)
req.post(this.url, this.contentType, this.options.headers, reqBody, this.options.timeout, this.options.httpAgent, this.options.httpsAgent)
.then(() => {
// No need to clear the batch if batching is disabled
logEntry === undefined && this.clearBatch()
Expand Down
6 changes: 4 additions & 2 deletions src/requests.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const http = require('http')
const https = require('https')

const post = async (lokiUrl, contentType, headers = {}, data = '', timeout) => {
const post = async (lokiUrl, contentType, headers = {}, data = '', timeout, httpAgent, httpsAgent) => {
// Construct a buffer from the data string to have deterministic data size
const dataBuffer = Buffer.from(data, 'utf8')

Expand All @@ -14,6 +14,7 @@ const post = async (lokiUrl, contentType, headers = {}, data = '', timeout) => {
return new Promise((resolve, reject) => {
// Decide which http library to use based on the url
const lib = lokiUrl.protocol === 'https:' ? https : http
const agent = lokiUrl.protocol === 'https:' ? httpsAgent : httpAgent

// Construct the node request options
const options = {
Expand All @@ -22,7 +23,8 @@ const post = async (lokiUrl, contentType, headers = {}, data = '', timeout) => {
path: lokiUrl.pathname,
method: 'POST',
headers: Object.assign(defaultHeaders, headers),
timeout: timeout
timeout: timeout,
agent: agent
}

// Construct the request
Expand Down

0 comments on commit 32795fc

Please sign in to comment.