@thotischner/observability-mcp 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,7 @@ export declare class LokiConnector implements ObservabilityConnector {
18
18
  listServices(): Promise<ServiceInfo[]>;
19
19
  queryLogs(params: LogQuery): Promise<LogResult>;
20
20
  private getLabelValues;
21
- private resolveServiceLabel;
21
+ private resolveServiceSelector;
22
22
  private parseLine;
23
23
  private extractTopPatterns;
24
24
  private parseTimeRange;
@@ -61,10 +61,14 @@ export class LokiConnector {
61
61
  const seen = new Map();
62
62
  for (const label of this.serviceLabels) {
63
63
  const values = await this.getLabelValues(label);
64
- for (const name of values) {
65
- if (!seen.has(name)) {
66
- seen.set(name, {
67
- name,
64
+ for (const raw of values) {
65
+ // Docker's loki.source.docker writes container names with a leading '/'
66
+ // (Docker API Names[0] convention). Strip it for display so the name
67
+ // matches what the service-name validator and users will pass back in.
68
+ const display = label === "container" ? raw.replace(/^\//, "") : raw;
69
+ if (!seen.has(display)) {
70
+ seen.set(display, {
71
+ name: display,
68
72
  source: this.name,
69
73
  signalType: "logs",
70
74
  labels: { discoveredVia: label },
@@ -77,11 +81,12 @@ export class LokiConnector {
77
81
  async queryLogs(params) {
78
82
  const { start, end } = this.parseTimeRange(params.duration);
79
83
  const limit = Math.min(Math.max(params.limit || 100, 1), 1000);
80
- // Resolve which label this service identifier lives under. Falls back to
81
- // the first configured label when no exact match is found, preserving
82
- // legacy behavior for callers passing labels that aren't in the cache yet.
83
- const matchedLabel = await this.resolveServiceLabel(params.service);
84
- const service = this.escapeLogQLValue(params.service);
84
+ // Resolve label + actual selector value. For the 'container' label the
85
+ // value stored in Loki may be '/my-app-1' while the caller passes the
86
+ // sanitized 'my-app-1' return the prefixed form so the LogQL selector
87
+ // matches the real stream.
88
+ const { label: matchedLabel, value: rawValue } = await this.resolveServiceSelector(params.service);
89
+ const service = this.escapeLogQLValue(rawValue);
85
90
  let logql = `{${matchedLabel}="${service}"}`;
86
91
  if (params.level) {
87
92
  const level = this.escapeLogQLValue(params.level);
@@ -148,13 +153,18 @@ export class LokiConnector {
148
153
  return [];
149
154
  }
150
155
  }
151
- async resolveServiceLabel(service) {
156
+ async resolveServiceSelector(service) {
152
157
  for (const label of this.serviceLabels) {
153
158
  const values = await this.getLabelValues(label);
154
159
  if (values.includes(service))
155
- return label;
160
+ return { label, value: service };
161
+ // Container label values are Docker-prefixed with '/'. The caller can't
162
+ // pass that form (validator rejects '/'), so probe the prefixed variant.
163
+ if (label === "container" && values.includes(`/${service}`)) {
164
+ return { label, value: `/${service}` };
165
+ }
156
166
  }
157
- return this.serviceLabels[0] || "service_name";
167
+ return { label: this.serviceLabels[0] || "service_name", value: service };
158
168
  }
159
169
  parseLine(line) {
160
170
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thotischner/observability-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Unified observability gateway for AI agents — one MCP server for Prometheus, Loki, and any backend",
5
5
  "type": "module",
6
6
  "license": "MIT",