Skip to content

Commit 3500ee3

Browse files
Initial: yoga-opentelemetry
0 parents  commit 3500ee3

File tree

15 files changed

+1095
-0
lines changed

15 files changed

+1095
-0
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: CI
2+
on: [push, pull_request]
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
9+
- run: spago build

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.spago/
3+
output*/

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "purescript-yoga-opentelemetry",
3+
"version": "0.1.0",
4+
"description": "PureScript FFI bindings for OpenTelemetry",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/rowtype-yoga/purescript-yoga-backend-stack.git",
9+
"directory": "packages/yoga-opentelemetry"
10+
},
11+
"keywords": ["purescript", "ffi", "bindings", "yoga"]
12+
}

spago.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
workspace:
2+
package_set:
3+
registry: 51.1.0
4+
5+
package:
6+
name: yoga-opentelemetry
7+
publish:
8+
version: 0.1.0
9+
license: MIT
10+
location:
11+
githubOwner: rowtype-yoga
12+
githubRepo: purescript-yoga-opentelemetry
13+
dependencies:
14+
- js-promise: ">=1.0.0 <2.0.0"
15+
- js-promise-aff: ">=1.0.0 <2.0.0"
16+
- aff: ">=7.0.0 <8.0.0"
17+
- effect: ">=4.0.0 <5.0.0"
18+
- prelude: ">=6.0.0 <7.0.0"
19+
- datetime: ">=6.0.0 <7.0.0"
20+
- foreign-object: ">=4.0.0 <5.0.0"
21+
test:
22+
main: Test.Opentelemetry.Main
23+
dependencies:
24+
- spec: ">=7.0.0 <8.0.0"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base');
2+
3+
// Create console exporter
4+
export const createConsoleExporter = () => {
5+
return new ConsoleSpanExporter();
6+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Yoga.OpenTelemetry.Exporter.Console where
2+
3+
import Effect (Effect)
4+
import Yoga.OpenTelemetry.OpenTelemetry (SpanExporter)
5+
6+
-- Create console exporter (logs spans to console)
7+
foreign import createConsoleExporter :: Effect SpanExporter
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
2+
3+
// Create OTLP exporter
4+
export const createOTLPExporterImpl = (config) => {
5+
const otlpConfig = {};
6+
7+
if (config.url) {
8+
otlpConfig.url = config.url;
9+
}
10+
11+
return new OTLPTraceExporter(otlpConfig);
12+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Yoga.OpenTelemetry.Exporter.OTLP where
2+
3+
import Prelude
4+
5+
import Data.Newtype (class Newtype)
6+
import Effect (Effect)
7+
import Effect.Uncurried (EffectFn1, runEffectFn1)
8+
import Yoga.OpenTelemetry.OpenTelemetry (SpanExporter)
9+
import Prim.Row (class Union)
10+
11+
-- OTLP-specific types
12+
newtype OTLPEndpoint = OTLPEndpoint String
13+
14+
derive instance Newtype OTLPEndpoint _
15+
derive newtype instance Eq OTLPEndpoint
16+
derive newtype instance Show OTLPEndpoint
17+
18+
-- OTLP exporter configuration
19+
type OTLPExporterConfigImpl =
20+
( url :: OTLPEndpoint
21+
)
22+
23+
-- Create OTLP exporter
24+
foreign import createOTLPExporterImpl :: forall opts. EffectFn1 { | opts } SpanExporter
25+
26+
createOTLPExporter :: forall opts opts_. Union opts opts_ OTLPExporterConfigImpl => { | opts } -> Effect SpanExporter
27+
createOTLPExporter opts = runEffectFn1 createOTLPExporterImpl opts
28+
29+
-- Convenience function with defaults for local Jaeger
30+
-- Jaeger's OTLP HTTP endpoint is at port 4318
31+
createOTLPExporterDefaults :: Effect SpanExporter
32+
createOTLPExporterDefaults = createOTLPExporter
33+
{ url: OTLPEndpoint "http://localhost:4318/v1/traces" }

src/Yoga/OpenTelemetry/Logs.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { logs } from '@opentelemetry/api-logs';
2+
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
3+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
4+
import { Resource } from '@opentelemetry/resources';
5+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
6+
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
7+
8+
/**
9+
* Create a LoggerProvider with OTLP exporter
10+
* @param {Object} config - Configuration object
11+
* @param {Object} config.resource - Resource attributes
12+
* @param {string} config.resource.serviceName - Service name
13+
* @param {string} config.resource.serviceVersion - Service version
14+
* @param {string} config.resource.serviceNamespace - Service namespace
15+
* @param {Object} config.exporter - Exporter configuration
16+
* @param {string} config.exporter.url - OTLP endpoint URL
17+
* @returns {LoggerProvider} The logger provider instance
18+
*/
19+
export const createLoggerProviderImpl = (config) => {
20+
const resource = new Resource({
21+
[ATTR_SERVICE_NAME]: config.resource.serviceName,
22+
[ATTR_SERVICE_VERSION]: config.resource.serviceVersion,
23+
'service.namespace': config.resource.serviceNamespace, // Use string literal for namespace
24+
});
25+
26+
const logExporter = new OTLPLogExporter({
27+
url: config.exporter.url,
28+
});
29+
30+
const loggerProvider = new LoggerProvider({
31+
resource,
32+
logRecordProcessors: [new BatchLogRecordProcessor(logExporter)]
33+
});
34+
35+
return loggerProvider;
36+
};
37+
38+
/**
39+
* Register the logger provider globally
40+
* @param {LoggerProvider} provider - The logger provider to register
41+
* @returns {void}
42+
*/
43+
export const registerLoggerProviderImpl = (provider) => {
44+
logs.setGlobalLoggerProvider(provider);
45+
};
46+
47+
/**
48+
* Create and enable Pino instrumentation to send logs to OpenTelemetry
49+
* This must be called BEFORE creating any Pino loggers
50+
* @returns {PinoInstrumentation} The instrumentation instance
51+
*/
52+
export const createPinoInstrumentationImpl = () => {
53+
const instrumentation = new PinoInstrumentation({
54+
logHook: (span, record, level) => {
55+
// Optionally correlate logs with active span
56+
if (span) {
57+
record['trace_id'] = span.spanContext().traceId;
58+
record['span_id'] = span.spanContext().spanId;
59+
record['trace_flags'] = span.spanContext().traceFlags;
60+
}
61+
},
62+
});
63+
64+
instrumentation.enable();
65+
return instrumentation;
66+
};
67+
68+
/**
69+
* Hook up an existing Pino logger to OpenTelemetry
70+
* Note: This is for already-created loggers. Ideally use instrumentation before logger creation.
71+
* @param {Object} logger - Pino logger instance
72+
* @returns {void}
73+
*/
74+
export const hookPinoToOtelImpl = (logger) => {
75+
// The instrumentation should handle this automatically when enabled before logger creation
76+
// This function exists for compatibility but the instrumentation approach is preferred
77+
console.warn('hookPinoToOtelImpl called - prefer enabling instrumentation before logger creation');
78+
};

src/Yoga/OpenTelemetry/Logs.purs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module Yoga.OpenTelemetry.Logs
2+
( LoggerProvider
3+
, OTLPLogsEndpoint(..)
4+
, createLoggerProvider
5+
, registerLoggerProvider
6+
, createPinoInstrumentation
7+
, PinoInstrumentation
8+
) where
9+
10+
import Prelude
11+
12+
import Effect (Effect)
13+
import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
14+
import Yoga.OpenTelemetry.OpenTelemetry (ServiceName(..), ServiceVersion(..), ServiceNamespace(..))
15+
16+
-- Opaque types
17+
foreign import data LoggerProvider :: Type
18+
foreign import data PinoInstrumentation :: Type
19+
20+
newtype OTLPLogsEndpoint = OTLPLogsEndpoint String
21+
22+
derive newtype instance Eq OTLPLogsEndpoint
23+
derive newtype instance Show OTLPLogsEndpoint
24+
25+
-- FFI imports
26+
foreign import createLoggerProviderImpl
27+
:: EffectFn1
28+
{ resource ::
29+
{ serviceName :: String
30+
, serviceVersion :: String
31+
, serviceNamespace :: String
32+
}
33+
, exporter ::
34+
{ url :: String
35+
}
36+
}
37+
LoggerProvider
38+
39+
foreign import registerLoggerProviderImpl :: EffectFn1 LoggerProvider Unit
40+
41+
foreign import createPinoInstrumentationImpl :: Effect PinoInstrumentation
42+
43+
-- Public API
44+
createLoggerProvider
45+
:: { serviceName :: ServiceName
46+
, serviceVersion :: ServiceVersion
47+
, serviceNamespace :: ServiceNamespace
48+
, endpoint :: OTLPLogsEndpoint
49+
}
50+
-> Effect LoggerProvider
51+
createLoggerProvider { serviceName: ServiceName sn, serviceVersion: ServiceVersion sv, serviceNamespace: ServiceNamespace sns, endpoint: OTLPLogsEndpoint url } =
52+
runEffectFn1 createLoggerProviderImpl
53+
{ resource: { serviceName: sn, serviceVersion: sv, serviceNamespace: sns }
54+
, exporter: { url }
55+
}
56+
57+
registerLoggerProvider :: LoggerProvider -> Effect Unit
58+
registerLoggerProvider = runEffectFn1 registerLoggerProviderImpl
59+
60+
createPinoInstrumentation :: Effect PinoInstrumentation
61+
createPinoInstrumentation = createPinoInstrumentationImpl

0 commit comments

Comments
 (0)