mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-08-23 10:52:47 +02:00
zip code short circuit
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -33,6 +33,13 @@ processors:
|
|||||||
span:
|
span:
|
||||||
- 'status.code != STATUS_CODE_ERROR'
|
- 'status.code != STATUS_CODE_ERROR'
|
||||||
|
|
||||||
|
# Drop 404 log records to reduce noise
|
||||||
|
filter/drop_404_logs:
|
||||||
|
error_mode: ignore
|
||||||
|
logs:
|
||||||
|
log_record:
|
||||||
|
- 'attributes["http.response.status_code"] == 404'
|
||||||
|
|
||||||
exporters:
|
exporters:
|
||||||
otlp_http/grafana:
|
otlp_http/grafana:
|
||||||
endpoint: ${env:GRAFANA_OTLP_ENDPOINT}
|
endpoint: ${env:GRAFANA_OTLP_ENDPOINT}
|
||||||
@@ -60,7 +67,7 @@ service:
|
|||||||
exporters: [otlp_http/grafana]
|
exporters: [otlp_http/grafana]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
processors: [batch]
|
processors: [filter/drop_404_logs, batch]
|
||||||
exporters: [otlp_http/grafana]
|
exporters: [otlp_http/grafana]
|
||||||
metrics:
|
metrics:
|
||||||
receivers: [otlp]
|
receivers: [otlp]
|
||||||
|
|||||||
+13
-7
@@ -9,6 +9,7 @@ declare module 'fastify' {
|
|||||||
span?: Span;
|
span?: Span;
|
||||||
traceId?: string;
|
traceId?: string;
|
||||||
errorHandled?: boolean;
|
errorHandled?: boolean;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ function classifyByStatus(statusCode: number): string {
|
|||||||
}
|
}
|
||||||
import cors from '@fastify/cors';
|
import cors from '@fastify/cors';
|
||||||
import { NominatimClient, NominatimResultSchema } from './services/NominatimClient';
|
import { NominatimClient, NominatimResultSchema } from './services/NominatimClient';
|
||||||
|
import { isZipCode } from './services/ZipCodeService';
|
||||||
import { GithubClient, SponsorsResponseSchema } from './services/GithubClient';
|
import { GithubClient, SponsorsResponseSchema } from './services/GithubClient';
|
||||||
import { TurnstileClient } from './services/TurnstileClient';
|
import { TurnstileClient } from './services/TurnstileClient';
|
||||||
import { ZammadClient, ContactMessageBodySchema, ContactMessageBody } from './services/ZammadClient';
|
import { ZammadClient, ContactMessageBodySchema, ContactMessageBody } from './services/ZammadClient';
|
||||||
@@ -80,13 +82,15 @@ const start = async () => {
|
|||||||
|
|
||||||
request.errorHandled = true;
|
request.errorHandled = true;
|
||||||
|
|
||||||
server.log.error({
|
if (statusCode !== 404) {
|
||||||
url: request.url,
|
server.log.error({
|
||||||
method: request.method,
|
url: request.url,
|
||||||
traceId: request.traceId,
|
method: request.method,
|
||||||
error: error.message,
|
traceId: request.traceId,
|
||||||
stack: error.stack,
|
error: error.message,
|
||||||
}, 'Request error');
|
stack: error.stack,
|
||||||
|
}, 'Request error');
|
||||||
|
}
|
||||||
|
|
||||||
reply.status(statusCode).send({ error: 'Internal Server Error' });
|
reply.status(statusCode).send({ error: 'Internal Server Error' });
|
||||||
});
|
});
|
||||||
@@ -136,6 +140,7 @@ const start = async () => {
|
|||||||
'http.route': route,
|
'http.route': route,
|
||||||
'http.request.method': request.method,
|
'http.request.method': request.method,
|
||||||
'http.response.status_code': statusCode,
|
'http.response.status_code': statusCode,
|
||||||
|
...(request.meta?.geocodeSource ? { 'geocode.source': request.meta.geocodeSource as string } : {}),
|
||||||
});
|
});
|
||||||
span.setAttribute('http.response.status_code', statusCode);
|
span.setAttribute('http.response.status_code', statusCode);
|
||||||
if (statusCode >= 500) {
|
if (statusCode >= 500) {
|
||||||
@@ -222,6 +227,7 @@ const start = async () => {
|
|||||||
},
|
},
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { query } = request.query as { query: string };
|
const { query } = request.query as { query: string };
|
||||||
|
request.meta = { geocodeSource: isZipCode(query) ? 'local_zip' : 'nominatim' };
|
||||||
reply.header('Cache-Control', 'public, max-age=86400, s-maxage=86400');
|
reply.header('Cache-Control', 'public, max-age=86400, s-maxage=86400');
|
||||||
const result = await nominatim.geocodePhrase(query, false, request.span);
|
const result = await nominatim.geocodePhrase(query, false, request.span);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createCache, Cache } from 'cache-manager';
|
|||||||
import { Type, Static } from '@sinclair/typebox';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
import { type Span, SpanKind, SpanStatusCode, context, trace } from '@opentelemetry/api';
|
import { type Span, SpanKind, SpanStatusCode, context, trace } from '@opentelemetry/api';
|
||||||
import { tracer, otelLogger, SeverityNumber } from '../telemetry';
|
import { tracer, otelLogger, SeverityNumber } from '../telemetry';
|
||||||
|
import { isZipCode, lookupZipCode } from './ZipCodeService';
|
||||||
const { DiskStore } = require('cache-manager-fs-hash');
|
const { DiskStore } = require('cache-manager-fs-hash');
|
||||||
|
|
||||||
export const NominatimResultSchema = Type.Object({
|
export const NominatimResultSchema = Type.Object({
|
||||||
@@ -56,6 +57,12 @@ export class NominatimClient {
|
|||||||
baseUrl = 'https://nominatim.openstreetmap.org/search';
|
baseUrl = 'https://nominatim.openstreetmap.org/search';
|
||||||
|
|
||||||
async geocodePhrase(query: string, includeGeoJson: boolean = false, parentSpan?: Span): Promise<NominatimResult[]> {
|
async geocodePhrase(query: string, includeGeoJson: boolean = false, parentSpan?: Span): Promise<NominatimResult[]> {
|
||||||
|
// Short-circuit for ZIP codes — serve from local data, no Nominatim call needed
|
||||||
|
if (isZipCode(query)) {
|
||||||
|
const zipResult = lookupZipCode(query.trim());
|
||||||
|
return zipResult ? [zipResult] : [];
|
||||||
|
}
|
||||||
|
|
||||||
const cacheKey = `geocode:${query}`;
|
const cacheKey = `geocode:${query}`;
|
||||||
const cached = await cache.get(cacheKey);
|
const cached = await cache.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
|||||||
@@ -106,14 +106,15 @@ export class ZammadClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTicket(payload: CreateTicketPayload): Promise<void> {
|
async createTicket(payload: CreateTicketPayload, parentSpan?: Span): Promise<void> {
|
||||||
const { name, email, topic, subject, message } = payload;
|
const { name, email, topic, subject, message } = payload;
|
||||||
const group = TOPIC_GROUP_MAP[topic];
|
const group = TOPIC_GROUP_MAP[topic];
|
||||||
|
|
||||||
|
const ctx = parentSpan ? trace.setSpan(context.active(), parentSpan) : context.active();
|
||||||
const span = tracer.startSpan('zammad.createTicket', {
|
const span = tracer.startSpan('zammad.createTicket', {
|
||||||
kind: SpanKind.CLIENT,
|
kind: SpanKind.CLIENT,
|
||||||
attributes: { 'peer.service': 'zammad', 'http.request.method': 'POST' },
|
attributes: { 'peer.service': 'zammad', 'http.request.method': 'POST' },
|
||||||
});
|
}, ctx);
|
||||||
try {
|
try {
|
||||||
const customerId = await this.upsertCustomer(name, email, span);
|
const customerId = await this.upsertCustomer(name, email, span);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { NominatimResult } from './NominatimClient';
|
||||||
|
|
||||||
|
// [lat, lon, city, stateAbbr]
|
||||||
|
type ZipEntry = [number, number, string, string];
|
||||||
|
type ZipData = Record<string, ZipEntry>;
|
||||||
|
|
||||||
|
// Load data synchronously at module init — 1.7MB uncompressed, fast
|
||||||
|
const zipData: ZipData = JSON.parse(
|
||||||
|
readFileSync(join(__dirname, '../data/zipcodes-us.json'), 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
const ZIP_PATTERN = /^\d{5}$/;
|
||||||
|
|
||||||
|
export function isZipCode(query: string): boolean {
|
||||||
|
return ZIP_PATTERN.test(query.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a 5-digit US ZIP code and return a Nominatim-compatible result,
|
||||||
|
* or null if the ZIP is not found in the local dataset.
|
||||||
|
*/
|
||||||
|
export function lookupZipCode(zip: string): NominatimResult | null {
|
||||||
|
const normalized = zip.trim().slice(0, 5);
|
||||||
|
const entry = zipData[normalized];
|
||||||
|
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
const [lat, lon, city, state] = entry;
|
||||||
|
const latStr = String(lat);
|
||||||
|
const lonStr = String(lon);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addresstype: 'postcode',
|
||||||
|
boundingbox: [
|
||||||
|
String(lat - 0.1),
|
||||||
|
String(lat + 0.1),
|
||||||
|
String(lon - 0.1),
|
||||||
|
String(lon + 0.1),
|
||||||
|
],
|
||||||
|
class: 'place',
|
||||||
|
display_name: `${normalized}, ${city}, ${state}, United States`,
|
||||||
|
importance: 0.3,
|
||||||
|
lat: latStr,
|
||||||
|
licence: 'Local ZIP Code Data',
|
||||||
|
lon: lonStr,
|
||||||
|
name: normalized,
|
||||||
|
address: {
|
||||||
|
postcode: normalized,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
country: 'United States',
|
||||||
|
country_code: 'us',
|
||||||
|
},
|
||||||
|
place_rank: 11,
|
||||||
|
type: 'postcode',
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user