Skip to content
Open
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
41 changes: 35 additions & 6 deletions examples/with-graphql-mongoose/apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
FROM node:22-alpine AS base

ENV SCOPE=@repo/api
ARG APP_PATH
ENV APP_PATH=$APP_PATH
ENV APP_PATH=apps/api

### Builder
FROM base AS builder
Expand Down Expand Up @@ -46,25 +45,55 @@ FROM base AS runner

# Environment and dependencies
ENV NODE_ENV production
RUN apk add --no-cache tzdata
RUN apk add --no-cache tzdata curl bash unzip libc6-compat libstdc++
RUN apk update
ENV TZ=America/Caracas
WORKDIR /app

# Install Grafana Alloy (detect architecture)
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
ALLOY_ARCH="amd64"; \
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
ALLOY_ARCH="arm64"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
curl -L -o /tmp/alloy.zip "https://github.com/grafana/alloy/releases/latest/download/alloy-linux-${ALLOY_ARCH}.zip" && \
unzip /tmp/alloy.zip -d /tmp && \
mv /tmp/alloy-linux-${ALLOY_ARCH} /usr/local/bin/alloy && \
chmod +x /usr/local/bin/alloy && \
rm /tmp/alloy.zip

# User
RUN addgroup --system --gid 1001 avilatek
RUN adduser --system --uid 1001 avilatek
USER avilatek
# Copy dist

# Copy dist and dependencies
COPY --from=installer /app/package*.json .
COPY --from=installer /app/node_modules ./node_modules
COPY --from=installer /app/packages ./packages
COPY --from=installer /app/${APP_PATH}/dist ./${APP_PATH}/dist
COPY --from=installer /app/${APP_PATH}/src/*.env* ./${APP_PATH}/src/*.env*

COPY --from=installer /app/${APP_PATH}/package.json ./${APP_PATH}/package.json

# Copy Alloy configuration
COPY --from=builder /app/${APP_PATH}/alloy.config ./${APP_PATH}/alloy.config

# Copy startup script
COPY --from=builder /app/${APP_PATH}/start.sh /app/start.sh
RUN chmod +x /app/start.sh


# Give permissions to alloy storage path and startup script
RUN mkdir -p /tmp/alloy && \
chown -R avilatek:avilatek /tmp/alloy && \
chown avilatek:avilatek /app/start.sh

ARG PORT
ENV PORT=$PORT
EXPOSE $PORT
WORKDIR /app/${APP_PATH}

CMD ["npm", "start"]
CMD ["/bin/bash", "/app/start.sh"]
136 changes: 136 additions & 0 deletions examples/with-graphql-mongoose/apps/api/alloy.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Grafana Alloy Configuration for API Application
// This configuration collects logs and metrics from the Node.js API

// ============================================================================
// LOGGING PIPELINE
// ============================================================================

// Collect logs from the application's log file
loki.source.file "app_logs" {
targets = [
{
__path__ = "/tmp/api.log",
job = env("APP_NAME"),
app = env("APP_NAME"),
env = env("ENVIRONMENT"),
instance = env("HOSTNAME"),
},
]

forward_to = [loki.process.add_labels.receiver]
}

// Process and enrich logs
loki.process "add_labels" {
// Add additional labels
stage.static_labels {
values = {
cluster = env("CLUSTER_NAME"),
service = env("APP_NAME"),
}
}

// Parse JSON logs if they exist
stage.json {
expressions = {
level = "level",
message = "msg",
timestamp = "time",
}
}

// Extract log level
stage.labels {
values = {
level = "level",
}
}

forward_to = [loki.write.default.receiver]
}

// Send logs to Loki
loki.write "default" {
endpoint {
url = env("LOKI_URL")

// Optional: Add basic auth if needed
basic_auth {
username = env("LOKI_USERNAME")
password = env("LOKI_PASSWORD")
}
}

external_labels = {
cluster = env("CLUSTER_NAME"),
}
}

// ============================================================================
// METRICS PIPELINE
// ============================================================================

// Scrape metrics from the application (if it exposes /metrics)
prometheus.scrape "app_metrics" {
targets = [
{
__address__ = "localhost:" + env("APP_PORT"),
job = env("APP_NAME"),
app = env("APP_NAME"),
env = env("ENVIRONMENT"),
},
]

forward_to = [prometheus.relabel.add_labels.receiver]
scrape_interval = "15s"
metrics_path = "/metrics"
}

// Add labels to metrics
prometheus.relabel "add_labels" {
rule {
target_label = "cluster"
replacement = env("CLUSTER_NAME")
}

rule {
target_label = "instance"
replacement = env("HOSTNAME")
}

forward_to = [prometheus.remote_write.default.receiver]
}

// Send metrics to Prometheus/Mimir
prometheus.remote_write "default" {
endpoint {
url = env("PROMETHEUS_REMOTE_WRITE_URL")

// Optional: Add basic auth if needed
basic_auth {
username = env("PROMETHEUS_USERNAME")
password = env("PROMETHEUS_PASSWORD")
}
}

external_labels = {
cluster = env("CLUSTER_NAME"),
env = env("ENVIRONMENT"),
}
}

// ============================================================================
// PROCESS METRICS (CPU, Memory, etc.)
// ============================================================================

// Collect host/container metrics
prometheus.exporter.unix "node" {
include_exporter_metrics = true
}

prometheus.scrape "node_metrics" {
targets = prometheus.exporter.unix.node.targets
forward_to = [prometheus.remote_write.default.receiver]
job_name = "node-exporter"
}

3 changes: 2 additions & 1 deletion examples/with-graphql-mongoose/apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@
"pino-pretty": "13.0.0",
"pino-loki": "2.6.0",
"postmark": "4.0.5",
"prom-client": "^15.1.3",
"@repo/feature-flags": "*",
"slug": "11.0.0",
"ts-node": "10.9.2",
"zod": "4.0.5"
}
}
}
46 changes: 22 additions & 24 deletions examples/with-graphql-mongoose/apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as Sentry from '@sentry/node';
import Fastify, { FastifyHttpOptions } from 'fastify';
import mongoose from 'mongoose';
import featureFlagsPlugin from './plugins/feature-flags';
import metricsPlugin from './plugins/metrics';
import {
TFeatureFlagProvider,
featureFlagProviders,
Expand Down Expand Up @@ -47,13 +48,7 @@ export async function createApp() {
config = {
logger: {
level: 'info',
transport: {
target: '@axiomhq/pino',
options: {
dataset: process.env.AXIOM_DATASET,
token: process.env.AXIOM_TOKEN,
},
},

},
};
}
Expand Down Expand Up @@ -91,23 +86,26 @@ export async function createApp() {
}),
});

await app.register(featureFlagsPlugin, {
provider,
postHog:
provider === featureFlagProviders.post_hog
? {
apiKey: process.env.POSTHOG_API_KEY!,
host: process.env.POSTHOG_HOST,
}
: undefined,
growthBook:
provider === featureFlagProviders.growth_book
? {
apiKey: process.env.GROWTHBOOK_API_KEY!,
apiHost: process.env.GROWTHBOOK_API_HOST,
}
: undefined,
});
await app.register(metricsPlugin);
if (process.env.FEATURE_FLAG_PROVIDER) {
await app.register(featureFlagsPlugin, {
provider,
postHog:
provider === featureFlagProviders.post_hog
? {
apiKey: process.env.POSTHOG_API_KEY!,
host: process.env.POSTHOG_HOST,
}
: undefined,
growthBook:
provider === featureFlagProviders.growth_book
? {
apiKey: process.env.GROWTHBOOK_API_KEY!,
apiHost: process.env.GROWTHBOOK_API_HOST,
}
: undefined,
});
}

await app.ready();

Expand Down
83 changes: 83 additions & 0 deletions examples/with-graphql-mongoose/apps/api/src/plugins/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
import { register, Counter, Histogram, collectDefaultMetrics } from 'prom-client';

// Create metrics
const httpRequestCounter = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});

const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});

const graphqlRequestCounter = new Counter({
name: 'graphql_requests_total',
help: 'Total number of GraphQL requests',
labelNames: ['operation_name', 'operation_type'],
});

// Collect default metrics (CPU, memory, etc.)
collectDefaultMetrics({ register });

const metricsPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
// Hook to track all HTTP requests
fastify.addHook('onRequest', async (request, reply) => {
// Store start time for duration calculation
request.requestStartTime = Date.now();
});

fastify.addHook('onResponse', async (request, reply) => {
const duration = (Date.now() - (request.requestStartTime || Date.now())) / 1000;

const route = request.routeOptions?.url || request.url;
const method = request.method;
const statusCode = reply.statusCode.toString();

// Increment request counter
httpRequestCounter.inc({
method,
route,
status_code: statusCode,
});

// Record request duration
httpRequestDuration.observe(
{
method,
route,
status_code: statusCode,
},
duration
);
});

// Endpoint to expose metrics
fastify.get('/metrics', async (request, reply) => {
reply.header('Content-Type', register.contentType);
return register.metrics();
});

// Health check endpoint
fastify.get('/health', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
};

// Extend FastifyRequest type to include requestStartTime
declare module 'fastify' {
interface FastifyRequest {
requestStartTime?: number;
}
}

export default fp(metricsPlugin, {
name: 'metrics',
});

export { httpRequestCounter, httpRequestDuration, graphqlRequestCounter, register };
1 change: 1 addition & 0 deletions examples/with-graphql-mongoose/apps/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export async function start() {

const port = parseInt(String(process.env.PORT || '3000'), 10);
const host = process.env.HOST || '0.0.0.0';
console.log(`Server running on ${host}:${port}`);
await server.listen({ host, port });

for (const signal of ['SIGINT', 'SIGTERM']) {
Expand Down
Loading